use crate::handle_resolver::HandleResolver; use crate::metrics::SharedMetricsPublisher; use crate::queue::{HandleResolutionWork, QueueAdapter}; use axum::{ Router, extract::{MatchedPath, State}, http::Request, middleware::{self, Next}, response::{Json, Response}, routing::get, }; use serde_json::json; use std::sync::Arc; use std::time::Instant; use tower_http::services::ServeDir; pub(crate) struct InnerAppContext { pub(crate) handle_resolver: Arc, pub(crate) handle_queue: Arc>, pub(crate) metrics: SharedMetricsPublisher, pub(crate) etag_seed: String, pub(crate) cache_control_header: Option, pub(crate) static_files_dir: String, } #[derive(Clone)] pub struct AppContext(pub(crate) Arc); impl AppContext { /// Create a new AppContext with the provided configuration. pub fn new( handle_resolver: Arc, handle_queue: Arc>, metrics: SharedMetricsPublisher, etag_seed: String, cache_control_header: Option, static_files_dir: String, ) -> Self { Self(Arc::new(InnerAppContext { handle_resolver, handle_queue, metrics, etag_seed, cache_control_header, static_files_dir, })) } // Internal accessor methods for handlers pub(super) fn etag_seed(&self) -> &str { &self.0.etag_seed } pub(super) fn cache_control_header(&self) -> Option<&str> { self.0.cache_control_header.as_deref() } pub(super) fn static_files_dir(&self) -> &str { &self.0.static_files_dir } } use axum::extract::FromRef; macro_rules! impl_from_ref { ($context:ty, $field:ident, $type:ty) => { impl FromRef<$context> for $type { fn from_ref(context: &$context) -> Self { context.0.$field.clone() } } }; } impl_from_ref!(AppContext, handle_resolver, Arc); impl_from_ref!( AppContext, handle_queue, Arc> ); impl_from_ref!(AppContext, metrics, SharedMetricsPublisher); /// Middleware to track HTTP request metrics async fn metrics_middleware( State(metrics): State, matched_path: Option, request: Request, next: Next, ) -> Response { let start = Instant::now(); let method = request.method().to_string(); let path = matched_path .as_ref() .map(|p| p.as_str().to_string()) .unwrap_or_else(|| "unknown".to_string()); // Process the request let response = next.run(request).await; // Calculate duration let duration_ms = start.elapsed().as_millis() as u64; let status_code = response.status().as_u16().to_string(); // Publish metrics with tags metrics .time_with_tags( "http.request.duration_ms", duration_ms, &[ ("method", &method), ("path", &path), ("status", &status_code), ], ) .await; response } pub fn create_router(app_context: AppContext) -> Router { let static_dir = app_context.static_files_dir().to_string(); Router::new() .route("/xrpc/_health", get(handle_xrpc_health)) .route( "/xrpc/com.atproto.identity.resolveHandle", get(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle) .options(super::handle_xrpc_resolve_handle::handle_xrpc_resolve_handle_options), ) .fallback_service(ServeDir::new(static_dir)) .layer(middleware::from_fn_with_state( app_context.0.metrics.clone(), metrics_middleware, )) .with_state(app_context) } pub(super) async fn handle_xrpc_health() -> Json { Json(json!({ "version": "0.1.0", })) }