diff --git a/Cargo.lock b/Cargo.lock index 8c85b3c..efbd63b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -335,6 +335,21 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "adminrpc" +version = "0.2.9" +dependencies = [ + "anyhow", + "clap", + "config", + "deadpool-redis", + "observability", + "rpc", + "session_manager", + "tokio", + "tracing", +] + [[package]] name = "aead" version = "0.5.2" @@ -6795,10 +6810,11 @@ dependencies = [ "prost 0.14.3", "prost-types 0.14.3", "session_manager", - "slog", "tokio", "tonic 0.14.5", + "tonic-prost", "tonic-prost-build", + "tracing", "uuid", ] @@ -7589,9 +7605,9 @@ dependencies = [ "redis", "serde", "serde_json", - "slog", "thiserror 2.0.18", "tokio", + "tracing", "uuid", ] diff --git a/Cargo.toml b/Cargo.toml index f454298..1d8383d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ members = [ "libs/agent-tool-derive", "apps/migrate", "apps/app", + "apps/adminrpc", "apps/git-hook", "apps/gitserver", "apps/email", diff --git a/apps/adminrpc/Cargo.toml b/apps/adminrpc/Cargo.toml new file mode 100644 index 0000000..e1a004f --- /dev/null +++ b/apps/adminrpc/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "adminrpc" +version.workspace = true +edition.workspace = true +authors.workspace = true +description.workspace = true +repository.workspace = true +readme.workspace = true +homepage.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +documentation.workspace = true + +[[bin]] +name = "adminrpc" +path = "src/main.rs" + +[dependencies] +# gRPC +rpc = { workspace = true } + +# Session / Redis +session_manager = { workspace = true } +deadpool-redis = { workspace = true, features = ["cluster"] } + +# Observability +observability = { workspace = true } +tracing = { workspace = true } + +# Config +config = { workspace = true } + +# Utilities +anyhow = { workspace = true } + +# Async runtime +tokio = { workspace = true, features = ["rt-multi-thread", "signal"] } + +# CLI +clap = { workspace = true, features = ["derive"] } + +[lints] +workspace = true diff --git a/apps/adminrpc/src/args.rs b/apps/adminrpc/src/args.rs new file mode 100644 index 0000000..253df17 --- /dev/null +++ b/apps/adminrpc/src/args.rs @@ -0,0 +1,9 @@ +use clap::Parser; + +#[derive(Parser, Debug)] +#[command(name = "adminrpc")] +pub struct Args { + /// Override the bind address (default: 0.0.0.0:9090) + #[arg(short, long)] + pub bind: Option, +} diff --git a/apps/adminrpc/src/main.rs b/apps/adminrpc/src/main.rs new file mode 100644 index 0000000..50b69d1 --- /dev/null +++ b/apps/adminrpc/src/main.rs @@ -0,0 +1,68 @@ +use std::net::SocketAddr; +use anyhow::Context as _; +use clap::Parser; +use config::AppConfig; +use deadpool_redis::{cluster, Runtime}; +use session_manager::{SessionManager, SessionStorage}; +use rpc::admin::server::{serve, DEFAULT_GRPC_PORT}; + +mod args; +use args::Args; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let cfg = AppConfig::load(); + let log_level = cfg.log_level().unwrap_or_else(|_| "info".to_string()); + observability::init_tracing_subscriber(&log_level); + + let args = Args::parse(); + let bind_addr: SocketAddr = args + .bind + .map(|s| s.parse()) + .unwrap_or_else(|| format!("0.0.0.0:{}", DEFAULT_GRPC_PORT).parse()) + .context("invalid bind address")?; + + tracing::info!( + app_name = %cfg.app_name().unwrap_or_default(), + bind_addr = %bind_addr, + "Starting admin RPC server" + ); + + // ── Phase 6: OTLP tracing ────────────────────────────────────────────── + let _otel_guard = if cfg.otel_enabled().unwrap_or(false) { + let endpoint = cfg.otel_endpoint().unwrap_or_else(|_| "http://localhost:4317".to_string()); + let service_name = cfg.otel_service_name().unwrap_or_else(|_| "adminrpc".to_string()); + let service_version = cfg.otel_service_version().unwrap_or_else(|_| "0.1.0".to_string()); + tracing::info!(endpoint = %endpoint, service = %service_name, "OTLP tracing enabled"); + let guard = observability::init_otlp(&endpoint, &service_name, &service_version, &log_level) + .map_err(|e| anyhow::anyhow!("OTLP init failed: {}", e))?; + guard + } else { + None + }; + + // Redis connection pool + let redis_url = cfg.redis_url()?; + tracing::info!(redis_url = %redis_url, "Connecting to Redis"); + let manager = cluster::Manager::new(vec![redis_url.clone()], false) + .map_err(|e| anyhow::anyhow!("failed to create redis cluster manager: {}", e))?; + let pool: cluster::Pool = cluster::Pool::builder(manager) + .max_size(16) + .runtime(Runtime::Tokio1) + .build() + .map_err(|e| anyhow::anyhow!("failed to build redis pool: {}", e))?; + + // Test connection + let _conn = pool.get().await + .context("redis pool connection failed")?; + tracing::info!("Redis connected"); + + let storage = SessionStorage::new(pool); + let session_manager = SessionManager::new(storage); + + tracing::info!(addr = %bind_addr, "Admin gRPC server listening"); + serve(bind_addr, session_manager).await?; + + tracing::info!("Admin RPC server stopped"); + Ok(()) +}