新增: MongoDB/Redis 代理工具

This commit is contained in:
2026-03-25 01:30:01 +08:00
parent f59ed9aae0
commit 780f683706
20 changed files with 6381 additions and 2 deletions

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
View 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

View 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
View 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
View 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
View 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
View 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
View 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
View 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(())
}