#![warn(clippy::pedantic)] #![allow(clippy::unused_async)] // axum handlers needs async, even if no awaiting happens use std::{collections::HashMap, fmt::Display, sync::Arc, time::Duration}; use axum::{ body::Body, extract::{MatchedPath, OriginalUri}, http::{uri::PathAndQuery, Request, Uri}, response::Response, }; use chrono::DateTime; use color_eyre::eyre::{Error, Result}; use config::Config; use post::Post; use tag::Tag; 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}; mod feed; mod handlers; mod helpers; mod hilighting; mod markdown; mod post; mod tag; #[derive(Default)] pub struct AppState { startup_time: DateTime, base_url: Uri, posts: HashMap, tags: HashMap, tera: Tera, } #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; let cfg = Config::builder() .add_source(config::File::with_name("config.toml")) .add_source(config::Environment::with_prefix("WEBSITE")) .build()?; init_tracing(&cfg); info!("Starting server..."); let app = init_app(&cfg).await?; axum::Server::bind(&cfg.get_string("bind_address")?.parse().unwrap()) .serve(app.into_make_service()) .await?; Ok(()) } #[instrument] async fn init_app(cfg: &Config) -> Result { let base_url: Uri = cfg.get_string("base_url")? .parse() .unwrap(); let tera = Tera::new("templates/**/*")?; let mut state = AppState { startup_time: chrono::offset::Utc::now(), base_url, tera, ..Default::default() }; let posts = post::load_all(&state).await?; let tags = tag::get_tags(posts.values()); state.posts = posts; state.tags = tags; let state = Arc::new(state); info!("Listening at {}", state.base_url); Ok(handlers::routes(&state) .layer(CorsLayer::permissive()) .layer(CompressionLayer::new()) .layer( tower_http::trace::TraceLayer::new_for_http() .make_span_with(make_span) .on_response(on_response), ) .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) -> Span { let uri = if let Some(OriginalUri(uri)) = request.extensions().get::() { uri } else { request.uri() }; let route = request .extensions() .get::() .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)] pub enum WebsiteError { NotFound, InternalError(Error), } impl std::error::Error for WebsiteError { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { match self { WebsiteError::NotFound => None, WebsiteError::InternalError(e) => Some(e.as_ref()), } } } impl Display for WebsiteError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { WebsiteError::NotFound => write!(f, "Not found"), WebsiteError::InternalError(e) => write!(f, "Internal error: {e}"), } } } impl From for WebsiteError { fn from(value: tera::Error) -> Self { WebsiteError::InternalError(value.into()) } } impl From for WebsiteError { fn from(value: color_eyre::Report) -> Self { WebsiteError::InternalError(value) } }