#![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 post::Post; use tag::Tag; use tera::Tera; use tower_http::{compression::CompressionLayer, cors::CorsLayer}; use tracing::{field::Empty, info_span, log::info, Span}; 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()?; init_tracing(); info!("Starting server..."); let base_url: Uri = option_env!("SITE_BASE_URL") .unwrap_or("http://localhost:8080") .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); let app = 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()); info!("Now listening at {}", state.base_url); axum::Server::bind(&"0.0.0.0:8080".parse().unwrap()) .serve(app.into_make_service()) .await?; Ok(()) } fn init_tracing() { let filter = EnvFilter::builder() .with_default_directive("into".parse().unwrap()) .from_env_lossy(); tracing_subscriber::registry() .with(filter) .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) } }