use std::path::PathBuf;
use std::sync::Arc;
use axum::Extension;
use axum::Router;
use axum::extract::Path;
use axum::response::IntoResponse;
use axum::response::Response;
use axum::routing::get;
use axum::routing::post;
use http::StatusCode;
use mime::TEXT_HTML_UTF_8;
use mime::TEXT_PLAIN_UTF_8;
use super::constants;
use super::export::Exporter;
use super::html_generator::HtmlGenerator;
use super::memory::MemoryService;
use super::static_resources::StaticResourceHandler;
#[cfg(test)]
mod tests;
const TEXT_YAML: &str = "text/yaml; charset=utf-8";
#[derive(Clone)]
struct DiagnosticsState {
memory: MemoryService,
static_resources: StaticResourceHandler,
router_config: Arc<str>,
supergraph_schema: Arc<String>,
output_directory: PathBuf,
}
pub(super) fn create_router(
output_directory: &std::path::Path,
router_config: Arc<str>,
supergraph_schema: Arc<String>,
) -> Router {
let state = DiagnosticsState {
memory: MemoryService::new(output_directory),
static_resources: StaticResourceHandler::new(),
router_config,
supergraph_schema,
output_directory: output_directory.to_path_buf(),
};
Router::new()
.route("/", get(handle_dashboard))
.route("/system_info.txt", get(handle_system_info))
.route("/router_config.yaml", get(handle_router_config))
.route("/supergraph.graphql", get(handle_supergraph_schema))
.route("/export", get(handle_export))
.route("/memory/status", get(handle_memory_status))
.route(
"/memory/dumps",
get(handle_memory_list_dumps).delete(handle_memory_clear_dumps),
)
.route("/memory/start", post(handle_memory_start))
.route("/memory/stop", post(handle_memory_stop))
.route("/memory/dump", post(handle_memory_dump))
.route(
"/memory/dumps/{filename}",
get(handle_memory_download_dump).delete(handle_memory_delete_dump),
)
.fallback(handle_fallback)
.layer(Extension(state))
}
fn result_to_response<T, E>(result: Result<T, E>, error_message: &str) -> Response
where
T: IntoResponse,
E: std::fmt::Display,
{
match result {
Ok(value) => value.into_response(),
Err(e) => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("{}: {}", error_message, e),
)
.into_response(),
}
}
fn not_found_response(message: impl std::fmt::Display) -> Response {
(StatusCode::NOT_FOUND, format!("{}", message)).into_response()
}
async fn handle_dashboard() -> Response {
let result = HtmlGenerator::new().and_then(|g| g.generate_dashboard_html());
match result {
Ok(html) => (
StatusCode::OK,
[(http::header::CONTENT_TYPE, TEXT_HTML_UTF_8.as_ref())],
html,
)
.into_response(),
Err(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Failed to generate dashboard",
)
.into_response(),
}
}
async fn handle_system_info() -> Response {
result_to_response(
super::system_info::collect().await.map(|info| {
(
StatusCode::OK,
[(http::header::CONTENT_TYPE, TEXT_PLAIN_UTF_8.as_ref())],
info,
)
}),
"Failed to collect system info",
)
}
async fn handle_router_config(Extension(state): Extension<DiagnosticsState>) -> Response {
(
StatusCode::OK,
[(http::header::CONTENT_TYPE, TEXT_YAML)],
state.router_config.to_string(),
)
.into_response()
}
async fn handle_supergraph_schema(Extension(state): Extension<DiagnosticsState>) -> Response {
(
StatusCode::OK,
[(http::header::CONTENT_TYPE, TEXT_PLAIN_UTF_8.as_ref())],
state.supergraph_schema.to_string(),
)
.into_response()
}
async fn handle_export(Extension(state): Extension<DiagnosticsState>) -> Response {
let exporter = Exporter::new(
super::Config {
enabled: true,
listen: constants::network::default_listen_addr().into(),
output_directory: state.output_directory.clone(),
},
state.supergraph_schema.clone(),
state.router_config.clone(),
);
result_to_response(exporter.export().await, "Export failed")
}
async fn handle_memory_status(Extension(state): Extension<DiagnosticsState>) -> Response {
result_to_response(
state.memory.handle_status().await,
"Failed to get memory status",
)
}
async fn handle_memory_list_dumps(Extension(state): Extension<DiagnosticsState>) -> Response {
result_to_response(
state.memory.handle_list_dumps().await,
"Failed to list dumps",
)
}
async fn handle_memory_clear_dumps(Extension(state): Extension<DiagnosticsState>) -> Response {
result_to_response(
state.memory.handle_clear_all_dumps().await,
"Failed to clear dumps",
)
}
async fn handle_memory_start(Extension(state): Extension<DiagnosticsState>) -> Response {
result_to_response(
state.memory.handle_start().await,
"Failed to start profiling",
)
}
async fn handle_memory_stop(Extension(state): Extension<DiagnosticsState>) -> Response {
result_to_response(state.memory.handle_stop().await, "Failed to stop profiling")
}
async fn handle_memory_dump(Extension(state): Extension<DiagnosticsState>) -> Response {
result_to_response(state.memory.handle_dump().await, "Failed to create dump")
}
async fn handle_memory_download_dump(
Extension(state): Extension<DiagnosticsState>,
Path(filename): Path<String>,
) -> Response {
state
.memory
.handle_download_dump(&filename)
.await
.map_or_else(
|e| not_found_response(format!("Dump not found: {}", e)),
|r| r.into_response(),
)
}
async fn handle_memory_delete_dump(
Extension(state): Extension<DiagnosticsState>,
Path(filename): Path<String>,
) -> Response {
state
.memory
.handle_delete_dump(&filename)
.await
.map_or_else(
|e| not_found_response(format!("Dump not found: {}", e)),
|r| r.into_response(),
)
}
async fn handle_fallback(
Extension(state): Extension<DiagnosticsState>,
uri: http::Uri,
) -> Response {
let path = uri.path().strip_prefix('/').unwrap_or(uri.path());
match state.static_resources.get_resource(path) {
Some((content, content_type)) => (
StatusCode::OK,
[(http::header::CONTENT_TYPE, content_type)],
content,
)
.into_response(),
None => (StatusCode::NOT_FOUND, "Not found").into_response(),
}
}