//! Pingora 代理 API 处理函数 //! //! 提供 Pingora 代理相关的 API 接口,主要用于文档展示和状态查询。 use axum::{ extract::{Path, Query, State}, http::StatusCode, response::Json, }; use chrono::{DateTime, Utc}; use serde::Deserialize; use std::sync::Arc; use tracing::{debug, info, warn}; use super::proxy_api::*; use crate::router::AppState; use std::sync::atomic::Ordering; /// Pingora 代理状态查询 #[utoipa::path( get, path = "/proxy/status", tag = "proxy", summary = "获取 Pingora 代理服务状态", description = "返回当前 Pingora 代理服务的运行状态和配置信息", responses( (status = 200, description = "成功获取代理状态", body = ProxyStatus), (status = 503, description = "代理服务未启用", body = ProxyErrorResponse) ) )] pub async fn proxy_status( State(state): State>, ) -> Result, (StatusCode, Json)> { if state.config.proxy_config.is_none() || state.pingora_service.is_none() { return Err(( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_DISABLED".to_string(), message: "Pingora proxy service is not enabled or unavailable".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), )); } let svc = state.pingora_service.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_SERVICE_UNAVAILABLE".to_string(), message: "Pingora proxy service instance is unavailable".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), ) })?; let conf = svc.config().clone(); // 收集后端列表 let backends_arc = svc.backends(); let backends_map = backends_arc.read().await; let backend_count = backends_map.len(); // 收集后端列表(从缓存快照) let health_map = svc.health_snapshot().await; let backends = backends_map .iter() .map(|(port, host)| { if let Some(health) = health_map.get(port) { let last_check_str = DateTime::::from(health.last_check).to_rfc3339(); BackendInfo { port: *port, host: host.clone(), health_status: health.status.as_str().to_string(), last_check: last_check_str, } } else { BackendInfo { port: *port, host: host.clone(), health_status: "unknown".to_string(), last_check: Utc::now().to_rfc3339(), } } }) .collect::>(); let status = ProxyStatus { status: "running".to_string(), listen_port: conf.listen_port, default_backend_port: conf.default_backend_port, default_backend_host: conf.backend_host.clone(), backends, load_balancer: LoadBalancerInfo { algorithm: if svc.use_round_robin { "round-robin".to_string() } else { "ketama".to_string() }, health_check_enabled: true, backend_count, }, }; info!( "Query proxy status: port {}, default backend: {}:{} (backend count: {})", status.listen_port, status.default_backend_host, status.default_backend_port, backend_count ); Ok(Json(status)) } /// Pingora 代理统计信息 #[utoipa::path( get, path = "/proxy/stats", tag = "proxy", summary = "获取 Pingora 代理统计信息", description = "返回代理服务的请求统计和性能指标", responses( (status = 200, description = "成功获取统计信息", body = ProxyStats), (status = 503, description = "代理服务未启用", body = ProxyErrorResponse) ) )] pub async fn proxy_stats( State(state): State>, ) -> Result, (StatusCode, Json)> { // 需要代理配置启用且服务可用 if state.config.proxy_config.is_none() || state.pingora_service.is_none() { return Err(( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_DISABLED".to_string(), message: "Pingora proxy service is not enabled or unavailable".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), )); } let svc = state.pingora_service.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_SERVICE_UNAVAILABLE".to_string(), message: "Pingora proxy service instance is unavailable".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), ) })?; let m = &svc.metrics; let total_requests = m.total_requests.load(Ordering::Relaxed); let successful_requests = m.successful_responses.load(Ordering::Relaxed); let failed_requests = m.failed_responses.load(Ordering::Relaxed); let avg_response_time_ms = m.avg_response_time_ms(); // 按端口统计 let snaps = m.port_snapshots(); let port_stats = snaps .into_iter() .map(|ps| { let total = ps.successes + ps.failures; let success_rate = if total == 0 { 0.0 } else { (ps.successes as f64) / (total as f64) }; let avg_ms = if total == 0 { 0.0 } else { (ps.total_response_time_ns as f64) / 1_000_000.0 / (total as f64) }; PortStats { port: ps.port, requests: ps.requests, success_rate, avg_response_time_ms: avg_ms, } }) .collect::>(); let stats = ProxyStats { total_requests, successful_requests, failed_requests, avg_response_time_ms, active_connections: m.active_connections.load(Ordering::Relaxed) as u32, port_stats, }; info!( "Query proxy stats: total requests {}, success {}, failed {}, avg response time {:.2}ms", stats.total_requests, stats.successful_requests, stats.failed_requests, stats.avg_response_time_ms ); Ok(Json(stats)) } /// Pingora 代理配置查询 #[utoipa::path( get, path = "/proxy/config", tag = "proxy", summary = "获取 Pingora 代理配置", description = "返回当前代理服务的配置信息", responses( (status = 200, description = "成功获取配置信息", body = ProxyConfig), (status = 503, description = "代理服务未启用", body = ProxyErrorResponse) ) )] pub async fn proxy_config( State(state): State>, ) -> Result, (StatusCode, Json)> { if state.config.proxy_config.is_none() || state.pingora_service.is_none() { return Err(( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_DISABLED".to_string(), message: "Pingora proxy service is not enabled or unavailable".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), )); } let svc = state.pingora_service.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_SERVICE_UNAVAILABLE".to_string(), message: "Pingora proxy service instance is unavailable".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), ) })?; let conf = svc.config(); let app_conf = &state.config; let hc_conf = &app_conf .proxy_config .as_ref() .ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_CONFIG_UNAVAILABLE".to_string(), message: "Pingora proxy config unavailable".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), ) })? .health_check; let config = ProxyConfig { listen_port: conf.listen_port, default_backend_port: conf.default_backend_port, default_backend_host: conf.backend_host.clone(), load_balancing_algorithm: if svc.use_round_robin { "round-robin".to_string() } else { "ketama".to_string() }, health_check: HealthCheckConfig { enabled: hc_conf.enabled, interval_seconds: hc_conf.interval_seconds as u32, timeout_seconds: hc_conf.timeout_seconds as u32, healthy_threshold: hc_conf.healthy_threshold, unhealthy_threshold: hc_conf.unhealthy_threshold, }, }; info!( "Query proxy config: listen port {}, default backend: {}:{}, LB algorithm: {}", config.listen_port, config.default_backend_host, config.default_backend_port, config.load_balancing_algorithm ); Ok(Json(config)) } /// 代理到指定端口(重定向到 Pingora) #[utoipa::path( get, path = "/proxy/{port}", tag = "proxy", summary = "重定向到 Pingora 代理(无路径)", description = r#" 重定向请求到 Pingora 代理服务,无额外路径。 ## 工作原理 此接口会返回 307 重定向,将请求转发到 Pingora 代理服务的实际端口。 ## 实际代理路径 真正的代理由 Pingora 处理,路径格式为: ``` GET /proxy/{port}/ ``` ## 使用示例 ```bash # 访问此接口 GET /proxy/3000 # 返回 307 重定向到: # http://127.0.0.1:{pingora_port}/proxy/3000/ # Pingora 代理到: # 127.0.0.1:3000/ ``` "#, params( ("port" = u16, Path, description = "目标端口号") ), responses( (status = 307, description = "重定向到 Pingora 代理服务", body = String), (status = 503, description = "代理服务未启用", body = ProxyErrorResponse) ) )] pub async fn proxy_to_port( State(state): State>, Path(port): Path, ) -> Result)> { if state.config.proxy_config.is_none() { return Err(( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_DISABLED".to_string(), message: "Pingora proxy service not enabled".to_string(), target_port: port, timestamp: Utc::now().to_rfc3339(), }), )); } let proxy_config = state.config.proxy_config.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_CONFIG_UNAVAILABLE".to_string(), message: "Pingora proxy config unavailable".to_string(), target_port: port, timestamp: Utc::now().to_rfc3339(), }), ) })?; let listen_port = proxy_config.listen_port; // 重定向到 Pingora 真实代理端口 let location = format!("http://127.0.0.1:{}/proxy/{}", listen_port, port); let resp = axum::http::Response::builder() .status(StatusCode::TEMPORARY_REDIRECT) .header(axum::http::header::LOCATION, location) .body(axum::body::Body::empty()) .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ProxyErrorResponse { error: "RESPONSE_BUILD_ERROR".to_string(), message: format!("failed to build response: {}", e), target_port: port, timestamp: Utc::now().to_rfc3339(), }), ) })?; Ok(resp) } /// 代理到指定端口和路径(重定向到 Pingora) #[utoipa::path( get, path = "/proxy/{port}/{*path}", tag = "proxy", summary = "重定向到 Pingora 代理(含路径)", description = r#" 重定向请求到 Pingora 代理服务,包含完整路径信息。 ## 工作原理 此接口会返回 307 重定向,将请求转发到 Pingora 代理服务的实际端口和路径。 ## 实际代理路径 真正的代理由 Pingora 处理,路径格式为: ``` GET /proxy/{port}/{path} ``` ## 使用示例 ```bash # 访问此接口 GET /proxy/8080/api/users # 返回 307 重定向到: # http://127.0.0.1:{pingora_port}/proxy/8080/api/users # Pingora 代理到: # 127.0.0.1:8080/api/users ``` "#, params( ("port" = u16, Path, description = "目标端口号"), ("path" = String, Path, description = "目标路径") ), responses( (status = 307, description = "重定向到 Pingora 代理服务", body = String), (status = 503, description = "代理服务未启用", body = ProxyErrorResponse) ) )] pub async fn proxy_to_port_with_path( State(state): State>, Path((port, path)): Path<(u16, String)>, ) -> Result)> { if state.config.proxy_config.is_none() { return Err(( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_DISABLED".to_string(), message: "Pingora proxy service not enabled".to_string(), target_port: port, timestamp: Utc::now().to_rfc3339(), }), )); } let proxy_config = state.config.proxy_config.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_CONFIG_UNAVAILABLE".to_string(), message: "Pingora proxy config unavailable".to_string(), target_port: port, timestamp: Utc::now().to_rfc3339(), }), ) })?; let listen_port = proxy_config.listen_port; let target_path = if path.is_empty() || path == "/" { "/".to_string() } else { format!("/{}", path) }; // 重定向到 Pingora 真实代理端口(保持相同的路径) let location = format!( "http://127.0.0.1:{}/proxy/{}{}", listen_port, port, target_path ); let resp = axum::http::Response::builder() .status(StatusCode::TEMPORARY_REDIRECT) .header(axum::http::header::LOCATION, location) .body(axum::body::Body::empty()) .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(ProxyErrorResponse { error: "RESPONSE_BUILD_ERROR".to_string(), message: format!("failed to build response: {}", e), target_port: port, timestamp: Utc::now().to_rfc3339(), }), ) })?; Ok(resp) } /// 通用代理请求处理器 async fn proxy_request_handler( state: Arc, port: u16, path: Option, ) -> Result, (StatusCode, Json)> { if state.config.proxy_config.is_none() { return Err(( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_DISABLED".to_string(), message: "Pingora proxy service not enabled".to_string(), target_port: port, timestamp: Utc::now().to_rfc3339(), }), )); } let proxy_config = state.config.proxy_config.as_ref().ok_or_else(|| { ( StatusCode::SERVICE_UNAVAILABLE, Json(ProxyErrorResponse { error: "PROXY_CONFIG_UNAVAILABLE".to_string(), message: "Pingora proxy config unavailable".to_string(), target_port: port, timestamp: Utc::now().to_rfc3339(), }), ) })?; let target_host = &proxy_config.backend_host; let target_path = path.unwrap_or_else(|| "/".to_string()); let target_url = format!("http://{}:{}{}", target_host, port, target_path); debug!("Mock proxy request: {} -> {}", port, target_url); // 这里只是用于文档展示,实际的代理由 Pingora 服务器处理 // 如果用户访问这些接口,我们会返回信息,说明实际的代理在 Pingora 服务器端口 let response = ProxyResponse { success: true, target_port: port, target_host: target_host.clone(), target_url: target_url.clone(), response_time_ms: Some(35), load_balancer: LoadBalancerInfo { algorithm: "round-robin".to_string(), health_check_enabled: true, backend_count: 1, }, }; info!( "Proxy request documentation demo: port {}, path {}, target: {}", port, target_path, target_url ); Ok(Json(response)) } /// 查询参数 #[derive(Debug, Deserialize, utoipa::IntoParams)] pub struct ProxyQueryParams { /// 端口号(用于向后兼容) #[param(example = 3000)] pub port: Option, /// 路径(可选) #[param(example = "/api/users")] pub path: Option, } /// 使用查询参数的代理方式(向后兼容) #[utoipa::path( get, path = "/proxy", tag = "proxy", summary = "使用查询参数代理(向后兼容)", description = "通过查询参数指定目标端口和路径,保持向后兼容性", params( ProxyQueryParams ), responses( (status = 200, description = "代理成功", body = ProxyResponse), (status = 400, description = "缺少端口参数", body = ProxyErrorResponse), (status = 503, description = "代理服务未启用", body = ProxyErrorResponse) ) )] pub async fn proxy_with_query_params( State(state): State>, Query(params): Query, ) -> Result, (StatusCode, Json)> { let port = params.port.ok_or_else(|| { ( StatusCode::BAD_REQUEST, Json(ProxyErrorResponse { error: "MISSING_PORT".to_string(), message: "Missing port parameter".to_string(), target_port: 0, timestamp: Utc::now().to_rfc3339(), }), ) })?; let path = params.path.clone().unwrap_or_else(|| "/".to_string()); warn!( "Using deprecated query parameter proxy method, recommended path format: /proxy/{}/{}", port, path ); proxy_request_handler(state, port, Some(path)).await }