feat(adminrpc): new standalone binary for admin gRPC service

Separate binary for Kubernetes internal admin RPC communication
(SessionAdmin service on port 9090). Includes:

- Redis cluster pool via session_manager
- OTLP tracing with env-driven configuration
- Tracing subscriber init (JSON to stderr)
- Graceful startup with connection verification
This commit is contained in:
ZhenYi 2026-04-21 23:06:11 +08:00
parent 4aaee59fa4
commit fbd228f17e
5 changed files with 140 additions and 2 deletions

20
Cargo.lock generated
View File

@ -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",
]

View File

@ -21,6 +21,7 @@ members = [
"libs/agent-tool-derive",
"apps/migrate",
"apps/app",
"apps/adminrpc",
"apps/git-hook",
"apps/gitserver",
"apps/email",

44
apps/adminrpc/Cargo.toml Normal file
View File

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

View File

@ -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<String>,
}

68
apps/adminrpc/src/main.rs Normal file
View File

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