新增: MongoDB/Redis 代理工具
This commit is contained in:
1491
redis-proxy/Cargo.lock
generated
Normal file
1491
redis-proxy/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
redis-proxy/Cargo.toml
Normal file
22
redis-proxy/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "redis-proxy"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Redis HTTP proxy with connection pooling"
|
||||
|
||||
[dependencies]
|
||||
redis = "0.27"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
axum = "0.7"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
toml = "0.8"
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
ureq = "2"
|
||||
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||
|
||||
[profile.release]
|
||||
opt-level = "z"
|
||||
lto = true
|
||||
strip = true
|
||||
14
redis-proxy/redis-proxy.toml
Normal file
14
redis-proxy/redis-proxy.toml
Normal file
@@ -0,0 +1,14 @@
|
||||
[server]
|
||||
port = 3310
|
||||
host = "127.0.0.1"
|
||||
|
||||
[pool]
|
||||
idle_timeout_secs = 300
|
||||
check_interval_secs = 60
|
||||
|
||||
[[connections]]
|
||||
name = "flux_dev"
|
||||
host = "39.99.243.191"
|
||||
port = 6379
|
||||
password = "suke123!"
|
||||
db = 0
|
||||
107
redis-proxy/src/cli.rs
Normal file
107
redis-proxy/src/cli.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use anyhow::{Result, bail};
|
||||
use serde::Deserialize;
|
||||
use std::time::Instant;
|
||||
|
||||
const DEFAULT_SERVER: &str = "http://127.0.0.1:3310";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ErrorResponse { error: String }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ConnectionsResponse { connections: Vec<ConnectionInfo> }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct ConnectionInfo { name: String, host: String, port: u16, db: i64, status: String }
|
||||
|
||||
pub struct Cli { server: String }
|
||||
|
||||
impl Cli {
|
||||
pub fn new(server: Option<String>) -> Self { Self { server: server.unwrap_or_else(|| DEFAULT_SERVER.to_string()) } }
|
||||
pub fn check_server(&self) -> Result<bool> { match ureq::get(&format!("{}/health", self.server)).call() { Ok(_) => Ok(true), Err(_) => Ok(false) } }
|
||||
|
||||
pub fn run(&self, conn: &str, command: &str, args: &[String]) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let body = serde_json::json!({"conn": conn, "command": command, "args": args});
|
||||
let resp = post_json(&format!("{}/run", self.server), &body)?;
|
||||
let total_ms = start.elapsed().as_millis();
|
||||
if let Ok(err) = serde_json::from_str::<ErrorResponse>(&resp) { eprintln!("Error: {}", err.error); return Ok(()); }
|
||||
#[derive(Deserialize)] struct RunResp { result: serde_json::Value, #[serde(rename = "durationMs")] duration_ms: u64 }
|
||||
let result: RunResp = serde_json::from_str(&resp)?;
|
||||
println!("{}", serde_json::to_string_pretty(&result.result).unwrap_or_default());
|
||||
println!("\n({}ms db, {}ms total)", result.duration_ms, total_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn get(&self, conn: &str, key: &str) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let body = serde_json::json!({"conn": conn, "key": key});
|
||||
let resp = post_json(&format!("{}/get", self.server), &body)?;
|
||||
let total_ms = start.elapsed().as_millis();
|
||||
if let Ok(err) = serde_json::from_str::<ErrorResponse>(&resp) { eprintln!("Error: {}", err.error); return Ok(()); }
|
||||
#[derive(Deserialize)] struct GetResp { value: Option<String>, #[serde(rename = "durationMs")] duration_ms: u64 }
|
||||
let result: GetResp = serde_json::from_str(&resp)?;
|
||||
match result.value { Some(v) => println!("\"{}\"", v), None => println!("(nil)") }
|
||||
println!("({}ms db, {}ms total)", result.duration_ms, total_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set(&self, conn: &str, key: &str, value: &str, ttl: Option<i64>) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let mut body = serde_json::json!({"conn": conn, "key": key, "value": value});
|
||||
if let Some(t) = ttl { body["ttl"] = serde_json::json!(t); }
|
||||
let resp = post_json(&format!("{}/set", self.server), &body)?;
|
||||
let total_ms = start.elapsed().as_millis();
|
||||
if let Ok(err) = serde_json::from_str::<ErrorResponse>(&resp) { eprintln!("Error: {}", err.error); return Ok(()); }
|
||||
#[derive(Deserialize)] struct SetResp { ok: bool, #[serde(rename = "durationMs")] duration_ms: u64 }
|
||||
let result: SetResp = serde_json::from_str(&resp)?;
|
||||
println!("OK ({}ms db, {}ms total)", result.duration_ms, total_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn del(&self, conn: &str, keys: &[String]) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let body = serde_json::json!({"conn": conn, "keys": keys});
|
||||
let resp = post_json(&format!("{}/del", self.server), &body)?;
|
||||
let total_ms = start.elapsed().as_millis();
|
||||
if let Ok(err) = serde_json::from_str::<ErrorResponse>(&resp) { eprintln!("Error: {}", err.error); return Ok(()); }
|
||||
#[derive(Deserialize)] struct DelResp { deleted: u64, #[serde(rename = "durationMs")] duration_ms: u64 }
|
||||
let result: DelResp = serde_json::from_str(&resp)?;
|
||||
println!("(integer) {}\n({}ms db, {}ms total)", result.deleted, result.duration_ms, total_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn keys(&self, conn: &str, pattern: &str) -> Result<()> {
|
||||
let start = Instant::now();
|
||||
let body = serde_json::json!({"conn": conn, "pattern": pattern});
|
||||
let resp = post_json(&format!("{}/keys", self.server), &body)?;
|
||||
let total_ms = start.elapsed().as_millis();
|
||||
if let Ok(err) = serde_json::from_str::<ErrorResponse>(&resp) { eprintln!("Error: {}", err.error); return Ok(()); }
|
||||
#[derive(Deserialize)] struct KeysResp { keys: Vec<String>, #[serde(rename = "durationMs")] duration_ms: u64 }
|
||||
let result: KeysResp = serde_json::from_str(&resp)?;
|
||||
for key in &result.keys { println!("{}", key); }
|
||||
println!("\n({} keys, {}ms db, {}ms total)", result.keys.len(), result.duration_ms, total_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn list_connections(&self) -> Result<()> {
|
||||
let response = ureq::get(&format!("{}/connections", self.server)).call()?;
|
||||
let body = response.into_string()?;
|
||||
if let Ok(err) = serde_json::from_str::<ErrorResponse>(&body) { bail!("{}", err.error); }
|
||||
let result: ConnectionsResponse = serde_json::from_str(&body)?;
|
||||
println!("Connections:");
|
||||
println!("{:<15} {:<25} {:<10} {:<5} {:<10}", "Name", "Host", "Port", "DB", "Status");
|
||||
println!("{}", "-".repeat(65));
|
||||
for conn in result.connections { println!("{:<15} {:<25} {:<10} {:<5} {:<10}", conn.name, conn.host, conn.port, conn.db, conn.status); }
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn post_json(url: &str, body: &serde_json::Value) -> Result<String> {
|
||||
let data = serde_json::to_string(body)?;
|
||||
let response = ureq::post(url).set("Content-Type", "application/json").send_string(&data);
|
||||
match response {
|
||||
Ok(r) => Ok(r.into_string()?),
|
||||
Err(ureq::Error::Status(_, resp)) => Ok(resp.into_string()?),
|
||||
Err(e) => bail!("HTTP error: {}", e),
|
||||
}
|
||||
}
|
||||
84
redis-proxy/src/config.rs
Normal file
84
redis-proxy/src/config.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use anyhow::{Result, bail};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ServerConfig {
|
||||
pub port: u16,
|
||||
#[serde(default = "default_host")]
|
||||
pub host: String,
|
||||
}
|
||||
|
||||
fn default_host() -> String { "127.0.0.1".to_string() }
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct PoolConfig {
|
||||
#[serde(default = "default_idle_timeout")]
|
||||
pub idle_timeout_secs: u64,
|
||||
#[serde(default = "default_check_interval")]
|
||||
pub check_interval_secs: u64,
|
||||
}
|
||||
|
||||
fn default_idle_timeout() -> u64 { 300 }
|
||||
fn default_check_interval() -> u64 { 60 }
|
||||
|
||||
impl Default for PoolConfig {
|
||||
fn default() -> Self {
|
||||
Self { idle_timeout_secs: default_idle_timeout(), check_interval_secs: default_check_interval() }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
pub struct ConnectionConfig {
|
||||
pub name: String,
|
||||
pub host: String,
|
||||
#[serde(default = "default_port")]
|
||||
pub port: u16,
|
||||
#[serde(default)]
|
||||
pub password: Option<String>,
|
||||
#[serde(default)]
|
||||
pub db: i64,
|
||||
}
|
||||
|
||||
pub fn default_port() -> u16 { 6379 }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
#[serde(default = "default_server")]
|
||||
pub server: ServerConfig,
|
||||
#[serde(default)]
|
||||
pub pool: PoolConfig,
|
||||
pub connections: Vec<ConnectionConfig>,
|
||||
}
|
||||
|
||||
fn default_server() -> ServerConfig {
|
||||
ServerConfig { port: 3310, host: "127.0.0.1".to_string() }
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn from_file(path: &str) -> Result<Self> {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
config.validate()?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn validate(&self) -> Result<()> {
|
||||
let mut names = std::collections::HashSet::new();
|
||||
for conn in &self.connections {
|
||||
if conn.name.is_empty() { bail!("Connection name cannot be empty"); }
|
||||
if names.contains(&conn.name) { bail!("Duplicate connection name: {}", conn.name); }
|
||||
names.insert(conn.name.clone());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionConfig {
|
||||
pub fn build_url(&self) -> String {
|
||||
match &self.password {
|
||||
Some(pass) => format!("redis://:{}@{}:{}/{}", pass, self.host, self.port, self.db),
|
||||
None => format!("redis://{}:{}/{}", self.host, self.port, self.db),
|
||||
}
|
||||
}
|
||||
}
|
||||
102
redis-proxy/src/db.rs
Normal file
102
redis-proxy/src/db.rs
Normal file
@@ -0,0 +1,102 @@
|
||||
use anyhow::{Result, bail};
|
||||
use redis::Client as RedisClient;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::config::{ConnectionConfig, PoolConfig};
|
||||
|
||||
struct ClientState {
|
||||
client: RedisClient,
|
||||
last_used: Instant,
|
||||
}
|
||||
|
||||
pub struct ConnectionManager {
|
||||
clients: Mutex<HashMap<String, ClientState>>,
|
||||
configs: Mutex<HashMap<String, ConnectionConfig>>,
|
||||
pool_config: PoolConfig,
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
pub fn new(configs: &[ConnectionConfig], pool_config: PoolConfig) -> Result<Self> {
|
||||
let mut config_map = HashMap::new();
|
||||
for cfg in configs {
|
||||
config_map.insert(cfg.name.clone(), cfg.clone());
|
||||
println!(" Registered: {} ({}:{}/{})", cfg.name, cfg.host, cfg.port, cfg.db);
|
||||
}
|
||||
println!("\n{} connection(s) configured (lazy init)", config_map.len());
|
||||
println!("Pool config: idle_timeout={}s", pool_config.idle_timeout_secs);
|
||||
Ok(Self { clients: Mutex::new(HashMap::new()), configs: Mutex::new(config_map), pool_config })
|
||||
}
|
||||
|
||||
pub fn get_conn(&self, name: &str) -> Result<(RedisClient, ConnectionConfig)> {
|
||||
let client = {
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
if let Some(state) = clients.get_mut(name) {
|
||||
state.last_used = Instant::now();
|
||||
Some(state.client.clone())
|
||||
} else { None }
|
||||
};
|
||||
if let Some(client) = client {
|
||||
let cfg = self.configs.lock().unwrap().get(name).unwrap().clone();
|
||||
return Ok((client, cfg));
|
||||
}
|
||||
let cfg = self.configs.lock().unwrap().get(name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Connection '{}' not found", name))?.clone();
|
||||
println!("[LazyInit] Creating Redis client for: {}", name);
|
||||
let client = self.create_client(&cfg)?;
|
||||
self.clients.lock().unwrap().insert(name.to_string(), ClientState { client: client.clone(), last_used: Instant::now() });
|
||||
Ok((client, cfg))
|
||||
}
|
||||
|
||||
fn create_client(&self, cfg: &ConnectionConfig) -> Result<RedisClient> {
|
||||
let url = cfg.build_url();
|
||||
let client = RedisClient::open(url.as_str())?;
|
||||
let mut conn = client.get_connection()?;
|
||||
redis::cmd("PING").query::<String>(&mut conn)?;
|
||||
println!("[LazyInit] ✓ Connected: {}", cfg.name);
|
||||
Ok(client)
|
||||
}
|
||||
|
||||
pub fn list_connections(&self) -> Vec<ConnectionInfo> {
|
||||
let clients = self.clients.lock().unwrap();
|
||||
let configs = self.configs.lock().unwrap();
|
||||
configs.iter().map(|(name, cfg)| {
|
||||
let status = if clients.contains_key(name) { "connected" } else { "pending" };
|
||||
ConnectionInfo { name: name.clone(), host: cfg.host.clone(), port: cfg.port, db: cfg.db, status: status.to_string() }
|
||||
}).collect()
|
||||
}
|
||||
|
||||
pub fn cleanup_idle(&self) {
|
||||
let mut clients = self.clients.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
let idle_timeout = Duration::from_secs(self.pool_config.idle_timeout_secs);
|
||||
clients.retain(|name, state| {
|
||||
let elapsed = now.duration_since(state.last_used);
|
||||
if elapsed > idle_timeout {
|
||||
println!("[Cleanup] Removing idle client: {} (idle {}s)", name, elapsed.as_secs());
|
||||
false
|
||||
} else { true }
|
||||
});
|
||||
}
|
||||
|
||||
pub fn add_connection(&self, cfg: ConnectionConfig) -> Result<()> {
|
||||
let name = cfg.name.clone();
|
||||
{ let configs = self.configs.lock().unwrap(); if configs.contains_key(&name) { bail!("Connection '{}' already exists", name); } }
|
||||
println!("[Dynamic] Adding: {} ({}:{}/{})", name, cfg.host, cfg.port, cfg.db);
|
||||
let client = self.create_client(&cfg)?;
|
||||
self.configs.lock().unwrap().insert(name.clone(), cfg);
|
||||
self.clients.lock().unwrap().insert(name.clone(), ClientState { client, last_used: Instant::now() });
|
||||
println!("[Dynamic] ✓ Added: {}", name);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct ConnectionInfo {
|
||||
pub name: String,
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
pub db: i64,
|
||||
pub status: String,
|
||||
}
|
||||
231
redis-proxy/src/handler.rs
Normal file
231
redis-proxy/src/handler.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use axum::{extract::State, http::StatusCode, Json};
|
||||
use redis::Commands;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::db::ConnectionManager;
|
||||
use crate::logger::{LogEntry, RequestLogger};
|
||||
|
||||
pub type AppState = Arc<(Arc<ConnectionManager>, Arc<RequestLogger>)>;
|
||||
|
||||
// ============== 请求 ==============
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RunRequest { pub conn: String, pub command: String, #[serde(default)] pub args: Vec<String> }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct GetRequest { pub conn: String, pub key: String }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SetRequest { pub conn: String, pub key: String, pub value: String, #[serde(default)] pub ttl: Option<i64> }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct DelRequest { pub conn: String, pub keys: Vec<String> }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct KeysRequest { pub conn: String, pub pattern: String }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct InfoRequest { pub conn: String }
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AddConnectionRequest { pub name: String, pub host: String, #[serde(default = "crate::config::default_port")] pub port: u16, pub password: Option<String>, #[serde(default)] pub db: i64 }
|
||||
|
||||
// ============== 响应 ==============
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct RunResponse { pub result: serde_json::Value, #[serde(rename = "durationMs")] pub duration_ms: u64 }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct GetResponse { pub value: Option<String>, #[serde(rename = "durationMs")] pub duration_ms: u64 }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct SetResponse { pub ok: bool, #[serde(rename = "durationMs")] pub duration_ms: u64 }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct DelResponse { pub deleted: u64, #[serde(rename = "durationMs")] pub duration_ms: u64 }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct KeysResponse { pub keys: Vec<String>, #[serde(rename = "durationMs")] pub duration_ms: u64 }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct InfoResponse { pub info: String, #[serde(rename = "durationMs")] pub duration_ms: u64 }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ConnectionsResponse { pub connections: Vec<crate::db::ConnectionInfo> }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct HealthResponse { pub status: String, pub connections: usize }
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ErrorResponse { pub error: String, #[serde(rename = "usage")] pub usage: Option<String> }
|
||||
|
||||
impl ErrorResponse {
|
||||
pub fn new(msg: &str) -> Self { Self { error: msg.to_string(), usage: None } }
|
||||
pub fn with_usage(mut self, usage: &str) -> Self { self.usage = Some(usage.to_string()); self }
|
||||
}
|
||||
|
||||
type ApiError = (StatusCode, Json<ErrorResponse>);
|
||||
|
||||
fn err(msg: &str) -> ApiError { (StatusCode::BAD_REQUEST, Json(ErrorResponse::new(msg))) }
|
||||
fn err_usage(msg: &str, usage: &str) -> ApiError { (StatusCode::BAD_REQUEST, Json(ErrorResponse::new(msg).with_usage(usage))) }
|
||||
|
||||
pub async fn run_cmd(State(state): State<AppState>, Json(req): Json<RunRequest>) -> Result<Json<RunResponse>, ApiError> {
|
||||
let (manager, logger) = state.as_ref();
|
||||
let start = Instant::now();
|
||||
let mgr = manager.clone();
|
||||
let conn_name = req.conn.clone();
|
||||
let cmd_str = format!("{} {}", req.command, req.args.join(" "));
|
||||
|
||||
let result: serde_json::Value = tokio::task::spawn_blocking(move || -> Result<serde_json::Value, ApiError> {
|
||||
let (client, _) = mgr.get_conn(&conn_name).map_err(|e| err_usage(&e.to_string(), "Usage: POST /run {\"conn\": \"name\", \"command\": \"GET\", \"args\": [\"key\"]}"))?;
|
||||
let mut conn = client.get_connection().map_err(|e| err(&e.to_string()))?;
|
||||
let mut cmd = redis::cmd(&req.command);
|
||||
for arg in &req.args { cmd.arg(arg); }
|
||||
let val: redis::Value = cmd.query(&mut conn).map_err(|e| err(&e.to_string()))?;
|
||||
Ok(redis_value_to_json(&val))
|
||||
}).await.map_err(|_| err("Task join error"))??;
|
||||
|
||||
let duration = start.elapsed();
|
||||
logger.log(&LogEntry::new("/run", "http").with_conn(&req.conn).with_command(&cmd_str).with_duration(duration.as_millis() as u64));
|
||||
Ok(Json(RunResponse { result, duration_ms: duration.as_millis() as u64 }))
|
||||
}
|
||||
|
||||
pub async fn get(State(state): State<AppState>, Json(req): Json<GetRequest>) -> Result<Json<GetResponse>, ApiError> {
|
||||
let (manager, logger) = state.as_ref();
|
||||
let start = Instant::now();
|
||||
let mgr = manager.clone();
|
||||
let conn_name = req.conn.clone();
|
||||
let key_name = req.key.clone();
|
||||
|
||||
let value: Option<String> = tokio::task::spawn_blocking(move || -> Result<Option<String>, ApiError> {
|
||||
let (client, _) = mgr.get_conn(&conn_name).map_err(|e| err(&e.to_string()))?;
|
||||
let mut conn = client.get_connection().map_err(|e| err(&e.to_string()))?;
|
||||
let val: Option<String> = conn.get(&key_name).map_err(|e| err(&e.to_string()))?;
|
||||
Ok(val)
|
||||
}).await.map_err(|_| err("Task join error"))??;
|
||||
|
||||
let duration = start.elapsed();
|
||||
logger.log(&LogEntry::new("/get", "http").with_conn(&req.conn).with_command(&format!("GET {}", req.key)).with_duration(duration.as_millis() as u64));
|
||||
Ok(Json(GetResponse { value, duration_ms: duration.as_millis() as u64 }))
|
||||
}
|
||||
|
||||
pub async fn set(State(state): State<AppState>, Json(req): Json<SetRequest>) -> Result<Json<SetResponse>, ApiError> {
|
||||
let (manager, logger) = state.as_ref();
|
||||
let start = Instant::now();
|
||||
let mgr = manager.clone();
|
||||
let conn_name = req.conn.clone();
|
||||
let key_name = req.key.clone();
|
||||
let value_str = req.value.clone();
|
||||
let ttl = req.ttl;
|
||||
|
||||
tokio::task::spawn_blocking(move || -> Result<(), ApiError> {
|
||||
let (client, _) = mgr.get_conn(&conn_name).map_err(|e| err(&e.to_string()))?;
|
||||
let mut conn = client.get_connection().map_err(|e| err(&e.to_string()))?;
|
||||
if let Some(secs) = ttl {
|
||||
conn.set_ex::<_, _, ()>(&key_name, &value_str, secs as u64).map_err(|e| err(&e.to_string()))?;
|
||||
} else {
|
||||
conn.set::<_, _, ()>(&key_name, &value_str).map_err(|e| err(&e.to_string()))?;
|
||||
}
|
||||
Ok(())
|
||||
}).await.map_err(|_| err("Task join error"))??;
|
||||
|
||||
let duration = start.elapsed();
|
||||
logger.log(&LogEntry::new("/set", "http").with_conn(&req.conn).with_command(&format!("SET {} {}", req.key, req.value)).with_duration(duration.as_millis() as u64));
|
||||
Ok(Json(SetResponse { ok: true, duration_ms: duration.as_millis() as u64 }))
|
||||
}
|
||||
|
||||
pub async fn del(State(state): State<AppState>, Json(req): Json<DelRequest>) -> Result<Json<DelResponse>, ApiError> {
|
||||
let (manager, logger) = state.as_ref();
|
||||
let start = Instant::now();
|
||||
let mgr = manager.clone();
|
||||
let conn_name = req.conn.clone();
|
||||
let del_keys = req.keys.clone();
|
||||
|
||||
let deleted: u64 = tokio::task::spawn_blocking(move || -> Result<u64, ApiError> {
|
||||
let (client, _) = mgr.get_conn(&conn_name).map_err(|e| err(&e.to_string()))?;
|
||||
let mut conn = client.get_connection().map_err(|e| err(&e.to_string()))?;
|
||||
let count: u64 = conn.del(&del_keys).map_err(|e| err(&e.to_string()))?;
|
||||
Ok(count)
|
||||
}).await.map_err(|_| err("Task join error"))??;
|
||||
|
||||
let duration = start.elapsed();
|
||||
logger.log(&LogEntry::new("/del", "http").with_conn(&req.conn).with_command(&format!("DEL {}", req.keys.join(" "))).with_duration(duration.as_millis() as u64));
|
||||
Ok(Json(DelResponse { deleted, duration_ms: duration.as_millis() as u64 }))
|
||||
}
|
||||
|
||||
pub async fn keys(State(state): State<AppState>, Json(req): Json<KeysRequest>) -> Result<Json<KeysResponse>, ApiError> {
|
||||
let (manager, logger) = state.as_ref();
|
||||
let start = Instant::now();
|
||||
let mgr = manager.clone();
|
||||
let conn_name = req.conn.clone();
|
||||
let pattern = req.pattern.clone();
|
||||
|
||||
let result: Vec<String> = tokio::task::spawn_blocking(move || -> Result<Vec<String>, ApiError> {
|
||||
let (client, _) = mgr.get_conn(&conn_name).map_err(|e| err(&e.to_string()))?;
|
||||
let mut conn = client.get_connection().map_err(|e| err(&e.to_string()))?;
|
||||
let keys: Vec<String> = conn.keys(&pattern).map_err(|e| err(&e.to_string()))?;
|
||||
Ok(keys)
|
||||
}).await.map_err(|_| err("Task join error"))??;
|
||||
|
||||
let duration = start.elapsed();
|
||||
logger.log(&LogEntry::new("/keys", "http").with_conn(&req.conn).with_command(&format!("KEYS {}", req.pattern)).with_duration(duration.as_millis() as u64));
|
||||
Ok(Json(KeysResponse { keys: result, duration_ms: duration.as_millis() as u64 }))
|
||||
}
|
||||
|
||||
pub async fn info(State(state): State<AppState>, Json(req): Json<InfoRequest>) -> Result<Json<InfoResponse>, ApiError> {
|
||||
let (manager, logger) = state.as_ref();
|
||||
let start = Instant::now();
|
||||
let mgr = manager.clone();
|
||||
let conn_name = req.conn.clone();
|
||||
|
||||
let info: String = tokio::task::spawn_blocking(move || -> Result<String, ApiError> {
|
||||
let (client, _) = mgr.get_conn(&conn_name).map_err(|e| err(&e.to_string()))?;
|
||||
let mut conn = client.get_connection().map_err(|e| err(&e.to_string()))?;
|
||||
let info: String = redis::cmd("INFO").query(&mut conn).map_err(|e| err(&e.to_string()))?;
|
||||
Ok(info)
|
||||
}).await.map_err(|_| err("Task join error"))??;
|
||||
|
||||
let duration = start.elapsed();
|
||||
logger.log(&LogEntry::new("/info", "http").with_conn(&req.conn).with_command("INFO").with_duration(duration.as_millis() as u64));
|
||||
Ok(Json(InfoResponse { info, duration_ms: duration.as_millis() as u64 }))
|
||||
}
|
||||
|
||||
pub async fn connections(State(state): State<AppState>) -> Json<ConnectionsResponse> {
|
||||
Json(ConnectionsResponse { connections: state.0.list_connections() })
|
||||
}
|
||||
|
||||
pub async fn health(State(state): State<AppState>) -> Json<HealthResponse> {
|
||||
Json(HealthResponse { status: "ok".to_string(), connections: state.0.list_connections().len() })
|
||||
}
|
||||
|
||||
pub async fn add_connection(State(state): State<AppState>, Json(req): Json<AddConnectionRequest>) -> Result<Json<serde_json::Value>, ApiError> {
|
||||
let (manager, logger) = state.as_ref();
|
||||
let cfg = crate::config::ConnectionConfig { name: req.name.clone(), host: req.host, port: req.port, password: req.password, db: req.db };
|
||||
manager.add_connection(cfg).map_err(|e| err(&e.to_string()))?;
|
||||
logger.log(&LogEntry::new("/connections/add", "http").with_conn(&req.name).with_duration(0));
|
||||
Ok(Json(serde_json::json!({"success": true, "message": format!("Connection '{}' added (temporary)", req.name)})))
|
||||
}
|
||||
|
||||
fn redis_value_to_json(val: &redis::Value) -> serde_json::Value {
|
||||
match val {
|
||||
redis::Value::Nil => serde_json::Value::Null,
|
||||
redis::Value::Int(i) => serde_json::json!(*i),
|
||||
redis::Value::BulkString(bytes) => {
|
||||
if let Ok(s) = std::str::from_utf8(bytes) {
|
||||
if (s.starts_with('{') && s.ends_with('}')) || (s.starts_with('[') && s.ends_with(']')) {
|
||||
serde_json::from_str(s).unwrap_or(serde_json::json!(s))
|
||||
} else {
|
||||
serde_json::json!(s)
|
||||
}
|
||||
} else {
|
||||
serde_json::json!(format!("{:?}", bytes))
|
||||
}
|
||||
}
|
||||
redis::Value::Array(values) => serde_json::json!(values.iter().map(redis_value_to_json).collect::<Vec<_>>()),
|
||||
redis::Value::SimpleString(s) => serde_json::json!(s),
|
||||
redis::Value::Okay => serde_json::json!("OK"),
|
||||
_ => serde_json::json!(format!("{:?}", val)),
|
||||
}
|
||||
}
|
||||
92
redis-proxy/src/logger.rs
Normal file
92
redis-proxy/src/logger.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::{File, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub struct RequestLogger {
|
||||
log_file: Mutex<Option<File>>,
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl RequestLogger {
|
||||
pub fn new(log_path: Option<&str>) -> Self {
|
||||
let (log_file, enabled) = if let Some(path) = log_path {
|
||||
let path = expand_path(path);
|
||||
if let Some(parent) = path.parent() { let _ = std::fs::create_dir_all(parent); }
|
||||
match OpenOptions::new().create(true).append(true).open(&path) {
|
||||
Ok(file) => (Some(file), true),
|
||||
Err(e) => { eprintln!("[Logger] Failed to open log file {}: {}", path.display(), e); (None, false) }
|
||||
}
|
||||
} else { (None, false) };
|
||||
Self { log_file: Mutex::new(log_file), enabled }
|
||||
}
|
||||
|
||||
pub fn is_enabled(&self) -> bool { self.enabled }
|
||||
|
||||
pub fn log(&self, entry: &LogEntry) {
|
||||
if !self.enabled { return; }
|
||||
let json = match serde_json::to_string(entry) {
|
||||
Ok(j) => j,
|
||||
Err(e) => { eprintln!("[Logger] Failed to serialize log: {}", e); return; }
|
||||
};
|
||||
if let Ok(mut file) = self.log_file.lock() {
|
||||
if let Some(ref mut f) = *file { let _ = writeln!(f, "{}", json); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LogEntry {
|
||||
pub timestamp: String,
|
||||
pub level: String,
|
||||
#[serde(rename = "type")]
|
||||
pub log_type: String,
|
||||
pub client: String,
|
||||
pub endpoint: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub conn: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub command: Option<String>,
|
||||
#[serde(rename = "durationMs")]
|
||||
pub duration_ms: u64,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
impl LogEntry {
|
||||
pub fn new(endpoint: &str, client: &str) -> Self {
|
||||
Self {
|
||||
timestamp: current_timestamp(), level: "INFO".to_string(), log_type: "request".to_string(),
|
||||
client: client.to_string(), endpoint: endpoint.to_string(),
|
||||
conn: None, command: None, duration_ms: 0, error: None,
|
||||
}
|
||||
}
|
||||
pub fn with_conn(mut self, conn: &str) -> Self { self.conn = Some(conn.to_string()); self }
|
||||
pub fn with_command(mut self, cmd: &str) -> Self { self.command = Some(truncate_string(cmd, 500)); self }
|
||||
pub fn with_duration(mut self, ms: u64) -> Self {
|
||||
self.duration_ms = ms;
|
||||
if ms > 5000 && self.level == "INFO" { self.level = "ERROR".to_string(); }
|
||||
else if ms > 2000 && self.level == "INFO" { self.level = "WARN".to_string(); }
|
||||
self
|
||||
}
|
||||
pub fn with_error(mut self, error: &str) -> Self { self.level = "ERROR".to_string(); self.error = Some(error.to_string()); self }
|
||||
}
|
||||
|
||||
fn current_timestamp() -> String {
|
||||
time::OffsetDateTime::now_utc()
|
||||
.format(&time::macros::format_description!("[year]-[month]-[day]T[hour]:[minute]:[second]"))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn truncate_string(s: &str, max_len: usize) -> String {
|
||||
if s.chars().count() <= max_len { s.to_string() }
|
||||
else { format!("{}... (truncated)", s.chars().take(max_len).collect::<String>()) }
|
||||
}
|
||||
|
||||
fn expand_path(path: &str) -> PathBuf {
|
||||
if path.starts_with('~') {
|
||||
let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).unwrap_or_default();
|
||||
PathBuf::from(path.replacen('~', &home, 1))
|
||||
} else { PathBuf::from(path) }
|
||||
}
|
||||
113
redis-proxy/src/main.rs
Normal file
113
redis-proxy/src/main.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
mod cli;
|
||||
mod config;
|
||||
mod db;
|
||||
mod handler;
|
||||
mod logger;
|
||||
|
||||
use axum::{routing::{get, post}, Router};
|
||||
use clap::{Parser, Subcommand};
|
||||
use std::sync::Arc;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(name = "redis-proxy")]
|
||||
#[command(about = "Redis HTTP proxy with connection pooling")]
|
||||
struct Args {
|
||||
#[command(subcommand)]
|
||||
command: Option<Commands>,
|
||||
#[arg(long, default_value = "redis-proxy.toml", global = true)]
|
||||
config: String,
|
||||
#[arg(short = 'S', long, default_value = "http://127.0.0.1:3310", global = true)]
|
||||
server: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Subcommand)]
|
||||
enum Commands {
|
||||
Server { #[arg(short = 'P', long)] port: Option<u16>, #[arg(short = 'H', long)] host: Option<String> },
|
||||
Run { #[arg(short, long)] conn: String, #[arg(short = 'C', long)] command: String, #[arg(short = 'a', long)] args: Vec<String> },
|
||||
Get { #[arg(short, long)] conn: String, #[arg(short = 'k', long)] key: String },
|
||||
Set { #[arg(short, long)] conn: String, #[arg(short = 'k', long)] key: String, #[arg(short, long)] value: String, #[arg(short, long)] ttl: Option<i64> },
|
||||
Del { #[arg(short, long)] conn: String, #[arg(short = 'k', long)] key: Vec<String> },
|
||||
Keys { #[arg(short, long)] conn: String, #[arg(short, long)] pattern: String },
|
||||
Connections,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let args = Args::parse();
|
||||
match args.command {
|
||||
Some(Commands::Server { port, host }) => run_server(&args.config, port, host).await,
|
||||
Some(Commands::Run { conn, command, args: ref keys }) => { let c = get_cli(&args.server)?; Ok(c.run(&conn, &command, keys)?) },
|
||||
Some(Commands::Get { conn, key }) => { let c = get_cli(&args.server)?; Ok(c.get(&conn, &key)?) },
|
||||
Some(Commands::Set { conn, key, value, ttl }) => { let c = get_cli(&args.server)?; Ok(c.set(&conn, &key, &value, ttl)?) },
|
||||
Some(Commands::Del { conn, key: ref keys }) => { let c = get_cli(&args.server)?; Ok(c.del(&conn, keys)?) },
|
||||
Some(Commands::Keys { conn, pattern }) => { let c = get_cli(&args.server)?; Ok(c.keys(&conn, &pattern)?) },
|
||||
Some(Commands::Connections) => { let c = get_cli(&args.server)?; Ok(c.list_connections()?) },
|
||||
None => run_server(&args.config, None, None).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_cli(server: &str) -> anyhow::Result<cli::Cli> {
|
||||
let c = cli::Cli::new(Some(server.to_string()));
|
||||
if !c.check_server()? {
|
||||
eprintln!("\n=== redis-proxy 未运行 ===\n启动代理: redis-proxy\n降级: redis-cli -h<host> -p<port> -a<password>");
|
||||
anyhow::bail!("Proxy server not running at {}", server);
|
||||
}
|
||||
Ok(c)
|
||||
}
|
||||
|
||||
async fn run_server(config_path: &str, port: Option<u16>, host: Option<String>) -> anyhow::Result<()> {
|
||||
println!("Redis HTTP Proxy v0.1.0\n");
|
||||
let mut config = config::Config::from_file(config_path)?;
|
||||
if let Some(port) = port { config.server.port = port; }
|
||||
if let Some(host) = host { config.server.host = host; }
|
||||
|
||||
let log_path = std::env::var("REDIS_PROXY_LOG").ok();
|
||||
let logger = Arc::new(logger::RequestLogger::new(log_path.as_deref()));
|
||||
if logger.is_enabled() { println!("Request logging: enabled"); }
|
||||
|
||||
println!("Initializing connection pools...\n");
|
||||
let manager = Arc::new(db::ConnectionManager::new(&config.connections, config.pool.clone())?);
|
||||
|
||||
let manager_clone = manager.clone();
|
||||
let check_interval = config.pool.check_interval_secs;
|
||||
tokio::spawn(async move {
|
||||
let mut interval = tokio::time::interval(std::time::Duration::from_secs(check_interval));
|
||||
loop { interval.tick().await; manager_clone.cleanup_idle(); }
|
||||
});
|
||||
|
||||
let app = Router::new()
|
||||
.route("/run", post(handler::run_cmd))
|
||||
.route("/get", post(handler::get))
|
||||
.route("/set", post(handler::set))
|
||||
.route("/del", post(handler::del))
|
||||
.route("/keys", post(handler::keys))
|
||||
.route("/info", post(handler::info))
|
||||
.route("/connections", get(handler::connections))
|
||||
.route("/connections/add", post(handler::add_connection))
|
||||
.route("/health", get(handler::health))
|
||||
.with_state(Arc::new((manager, logger)));
|
||||
|
||||
let addr = format!("{}:{}", config.server.host, config.server.port);
|
||||
let listener = tokio::net::TcpListener::bind(&addr).await?;
|
||||
|
||||
println!("\nServer started at http://{}", addr);
|
||||
println!("\nAPI Endpoints:");
|
||||
println!(" POST /run - Execute Redis command");
|
||||
println!(" POST /get - GET key");
|
||||
println!(" POST /set - SET key value [EX ttl]");
|
||||
println!(" POST /del - DEL key1 key2");
|
||||
println!(" POST /keys - KEYS pattern");
|
||||
println!(" POST /info - INFO");
|
||||
println!(" GET /connections - List connections");
|
||||
println!(" GET /health - Health check");
|
||||
println!("\nCLI Usage:");
|
||||
println!(" redis-proxy run -c flux_dev -C GET -a key");
|
||||
println!(" redis-proxy get -c flux_dev -k key");
|
||||
println!(" redis-proxy set -c flux_dev -k key -v value");
|
||||
println!(" redis-proxy del -c flux_dev -k key");
|
||||
println!(" redis-proxy keys -c flux_dev -p \"user:*\"");
|
||||
println!(" redis-proxy connections");
|
||||
|
||||
axum::serve(listener, app).await?;
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user