Files
qiming/qiming-rcoder/crates/rcoder/src/handler/proxy_handler_api.rs
2026-06-01 13:54:52 +08:00

586 lines
19 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<Arc<AppState>>,
) -> Result<Json<ProxyStatus>, (StatusCode, Json<ProxyErrorResponse>)> {
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::<Utc>::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::<Vec<_>>();
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<Arc<AppState>>,
) -> Result<Json<ProxyStats>, (StatusCode, Json<ProxyErrorResponse>)> {
// 需要代理配置启用且服务可用
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::<Vec<_>>();
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<Arc<AppState>>,
) -> Result<Json<ProxyConfig>, (StatusCode, Json<ProxyErrorResponse>)> {
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<Arc<AppState>>,
Path(port): Path<u16>,
) -> Result<axum::response::Response, (StatusCode, Json<ProxyErrorResponse>)> {
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<Arc<AppState>>,
Path((port, path)): Path<(u16, String)>,
) -> Result<axum::response::Response, (StatusCode, Json<ProxyErrorResponse>)> {
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<AppState>,
port: u16,
path: Option<String>,
) -> Result<Json<ProxyResponse>, (StatusCode, Json<ProxyErrorResponse>)> {
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<u16>,
/// 路径(可选)
#[param(example = "/api/users")]
pub path: Option<String>,
}
/// 使用查询参数的代理方式(向后兼容)
#[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<Arc<AppState>>,
Query(params): Query<ProxyQueryParams>,
) -> Result<Json<ProxyResponse>, (StatusCode, Json<ProxyErrorResponse>)> {
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
}