Add GrpcMetricsLayer from common::metrics to every gRPC service's Server::builder() chain, enabling automatic Prometheus instrumentation (grpc_server_started_total, grpc_server_handled_total, grpc_server_handling_seconds) for all RPC handlers. Services wired: - trading-service - api-gateway - ml-training-service - backtesting-service - broker-gateway - data-acquisition-service - trading-agent-service Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
589 lines
23 KiB
Rust
589 lines
23 KiB
Rust
#![deny(clippy::unwrap_used, clippy::expect_used)]
|
|
|
|
//! API Gateway Service
|
|
//!
|
|
//! High-performance gRPC gateway with 6-layer authentication and request routing.
|
|
//! Optimized for HFT requirements with <10μs authentication overhead.
|
|
|
|
use anyhow::Result;
|
|
use clap::Parser;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
use tracing::{error, info, warn};
|
|
|
|
use common::metrics::GrpcMetricsLayer;
|
|
|
|
// Import all needed types from the library
|
|
use api_gateway::auth::jwt::JwtConfig;
|
|
use api_gateway::auth::{
|
|
AuditLogger, AuthInterceptor, AuthzService, JwtService, RateLimiter, RevocationService,
|
|
};
|
|
|
|
#[derive(Parser, Debug)]
|
|
#[command(name = "api_gateway", about = "Foxhunt API Gateway Service")]
|
|
struct Args {
|
|
/// gRPC server bind address
|
|
#[arg(long, env = "GATEWAY_BIND_ADDR", default_value = "0.0.0.0:50051")]
|
|
bind_addr: String,
|
|
|
|
/// JWT secret (or use JWT_SECRET_FILE for production)
|
|
#[arg(long, env = "JWT_SECRET")]
|
|
jwt_secret: Option<String>,
|
|
|
|
/// JWT issuer
|
|
#[arg(long, env = "JWT_ISSUER", default_value = "foxhunt-trading")]
|
|
jwt_issuer: String,
|
|
|
|
/// JWT audience
|
|
#[arg(long, env = "JWT_AUDIENCE", default_value = "trading-api")]
|
|
jwt_audience: String,
|
|
|
|
/// Redis URL for JWT revocation
|
|
#[arg(long, env = "REDIS_URL", default_value = "redis://localhost:6379")]
|
|
redis_url: String,
|
|
|
|
/// Rate limit (requests per second per user)
|
|
#[arg(long, env = "RATE_LIMIT_RPS", default_value = "100")]
|
|
rate_limit_rps: u32,
|
|
|
|
/// Enable audit logging
|
|
#[arg(long, env = "ENABLE_AUDIT_LOGGING", default_value = "true")]
|
|
enable_audit_logging: bool,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let args = Args::parse();
|
|
|
|
// Initialize observability (JSON logging + OpenTelemetry tracing via OTLP)
|
|
let otlp_endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
.unwrap_or_else(|_| "http://localhost:4317".to_string());
|
|
if let Err(e) = common::observability::init_observability("api_gateway", &otlp_endpoint).await {
|
|
eprintln!("Failed to initialize observability: {}", e);
|
|
}
|
|
|
|
info!("Starting Foxhunt API Gateway Service");
|
|
info!("Bind address: {}", args.bind_addr);
|
|
info!("JWT issuer: {}", args.jwt_issuer);
|
|
info!("JWT audience: {}", args.jwt_audience);
|
|
info!("Redis URL: {}", args.redis_url);
|
|
info!("Rate limit: {} req/s per user", args.rate_limit_rps);
|
|
info!("Audit logging: {}", args.enable_audit_logging);
|
|
|
|
// Load JWT configuration (Vault-based with fallback)
|
|
info!("Loading JWT configuration...");
|
|
let jwt_config = match load_jwt_config().await {
|
|
Ok(config) => {
|
|
info!("✅ JWT configuration loaded successfully");
|
|
config
|
|
},
|
|
Err(e) => {
|
|
error!("❌ Failed to load JWT configuration: {}", e);
|
|
return Err(e);
|
|
},
|
|
};
|
|
|
|
// Initialize authentication components
|
|
info!("Initializing authentication services...");
|
|
|
|
let jwt_service = JwtService::new(
|
|
jwt_config.jwt_secret.clone(),
|
|
jwt_config.jwt_issuer.clone(),
|
|
jwt_config.jwt_audience.clone(),
|
|
);
|
|
let jwt_service_rest = JwtService::new(
|
|
jwt_config.jwt_secret.clone(),
|
|
jwt_config.jwt_issuer.clone(),
|
|
jwt_config.jwt_audience.clone(),
|
|
);
|
|
info!("✓ JWT service initialized with cached decoding key");
|
|
|
|
let revocation_service = RevocationService::new(&args.redis_url)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to connect to Redis for revocation service: {}", e))?;
|
|
let revocation_service_rest = RevocationService::new(&args.redis_url)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to connect to Redis for REST API: {}", e))?;
|
|
info!("✓ JWT revocation service connected to Redis");
|
|
|
|
let authz_service = AuthzService::new();
|
|
info!("✓ Authorization service initialized with permission cache");
|
|
|
|
let rate_limiter = RateLimiter::new(args.rate_limit_rps)
|
|
.map_err(|e| anyhow::anyhow!("Failed to create rate limiter: {}", e))?;
|
|
let rate_limiter_rest = RateLimiter::new(args.rate_limit_rps)
|
|
.map_err(|e| anyhow::anyhow!("Failed to create REST rate limiter: {}", e))?;
|
|
info!("✓ Rate limiter initialized ({} req/s)", args.rate_limit_rps);
|
|
|
|
let audit_logger = AuditLogger::new(args.enable_audit_logging);
|
|
info!("✓ Audit logger initialized");
|
|
|
|
// Create authentication interceptor
|
|
let auth_interceptor = AuthInterceptor::new(
|
|
jwt_service,
|
|
revocation_service,
|
|
authz_service,
|
|
rate_limiter,
|
|
audit_logger,
|
|
);
|
|
info!("✓ 6-layer authentication interceptor ready");
|
|
|
|
info!("API Gateway service initialization complete");
|
|
info!("Ready to accept gRPC requests with <10μs auth overhead");
|
|
|
|
// Initialize backend service proxies
|
|
info!("Connecting to backend services...");
|
|
|
|
let trading_backend_url = std::env::var("TRADING_SERVICE_URL")
|
|
.unwrap_or_else(|_| "http://localhost:50052".to_string());
|
|
let backtesting_backend_url = std::env::var("BACKTESTING_SERVICE_URL")
|
|
.unwrap_or_else(|_| "http://localhost:50053".to_string());
|
|
let ml_training_backend_url = std::env::var("ML_TRAINING_SERVICE_URL")
|
|
.unwrap_or_else(|_| "http://localhost:50054".to_string());
|
|
|
|
// Load TLS certificate paths for backtesting service (mTLS)
|
|
let backtesting_tls_ca_cert = std::env::var("BACKTESTING_TLS_CA_CERT").ok();
|
|
let backtesting_tls_client_cert = std::env::var("BACKTESTING_TLS_CLIENT_CERT").ok();
|
|
let backtesting_tls_client_key = std::env::var("BACKTESTING_TLS_CLIENT_KEY").ok();
|
|
|
|
// Load TLS certificate paths for ML training service (mTLS)
|
|
let ml_training_tls_ca_cert = std::env::var("ML_TRAINING_TLS_CA_CERT").ok();
|
|
let ml_training_tls_client_cert = std::env::var("ML_TRAINING_TLS_CLIENT_CERT").ok();
|
|
let ml_training_tls_client_key = std::env::var("ML_TRAINING_TLS_CLIENT_KEY").ok();
|
|
|
|
// Initialize trading service proxy
|
|
let trading_proxy = api_gateway::grpc::TradingServiceProxy::new_lazy(&trading_backend_url)
|
|
.map_err(|e| anyhow::anyhow!("Failed to create trading service proxy: {}", e))?;
|
|
info!(
|
|
"✓ Trading service proxy initialized ({})",
|
|
trading_backend_url
|
|
);
|
|
|
|
// Initialize backtesting service proxy (optional - graceful degradation)
|
|
info!("Attempting to initialize Backtesting Service proxy...");
|
|
info!(" Backend URL: {}", backtesting_backend_url);
|
|
info!(" CA cert: {:?}", backtesting_tls_ca_cert);
|
|
info!(" Client cert: {:?}", backtesting_tls_client_cert);
|
|
info!(" Client key: {:?}", backtesting_tls_client_key);
|
|
|
|
let backtesting_proxy = match api_gateway::grpc::BacktestingServiceProxy::new(
|
|
&backtesting_backend_url,
|
|
backtesting_tls_ca_cert.as_deref(),
|
|
backtesting_tls_client_cert.as_deref(),
|
|
backtesting_tls_client_key.as_deref(),
|
|
)
|
|
.await
|
|
{
|
|
Ok(proxy) => {
|
|
info!(
|
|
"✓ Backtesting service proxy initialized ({})",
|
|
backtesting_backend_url
|
|
);
|
|
Some(Arc::new(proxy))
|
|
},
|
|
Err(e) => {
|
|
error!("⚠ Backtesting service initialization failed!");
|
|
error!(" Error type: {:?}", e);
|
|
error!(" Error message: {}", e);
|
|
warn!("⚠ Backtesting service unavailable: {}. API Gateway will run without backtesting endpoints.", e);
|
|
warn!(" Backtesting endpoints will return 503 Service Unavailable");
|
|
None
|
|
},
|
|
};
|
|
|
|
// Spawn background health check task for backtesting service
|
|
if let Some(proxy) = backtesting_proxy.as_ref() {
|
|
let proxy_clone = Arc::clone(proxy);
|
|
tokio::spawn(async move {
|
|
let mut interval = tokio::time::interval(Duration::from_secs(10));
|
|
loop {
|
|
interval.tick().await;
|
|
proxy_clone.background_health_check().await;
|
|
}
|
|
});
|
|
info!("✓ Backtesting service health check task started (10s interval)");
|
|
}
|
|
|
|
// Initialize ML training service proxy (optional - graceful degradation)
|
|
info!("Attempting to initialize ML Training Service proxy...");
|
|
info!(" Backend URL: {}", ml_training_backend_url);
|
|
info!(" CA cert: {:?}", ml_training_tls_ca_cert);
|
|
info!(" Client cert: {:?}", ml_training_tls_client_cert);
|
|
info!(" Client key: {:?}", ml_training_tls_client_key);
|
|
|
|
let ml_config = api_gateway::grpc::MlTrainingBackendConfig {
|
|
address: ml_training_backend_url.clone(),
|
|
connect_timeout_ms: 5000,
|
|
request_timeout_ms: 30000,
|
|
circuit_breaker_failures: 5,
|
|
circuit_breaker_reset_secs: 30,
|
|
tls_ca_cert_path: ml_training_tls_ca_cert.clone(),
|
|
tls_client_cert_path: ml_training_tls_client_cert.clone(),
|
|
tls_client_key_path: ml_training_tls_client_key.clone(),
|
|
};
|
|
let ml_training_proxy = match api_gateway::grpc::setup_ml_training_proxy(ml_config).await {
|
|
Ok(proxy) => {
|
|
info!(
|
|
"✓ ML training service proxy initialized ({})",
|
|
ml_training_backend_url
|
|
);
|
|
Some(proxy)
|
|
},
|
|
Err(e) => {
|
|
error!("⚠ ML Training service initialization failed!");
|
|
error!(" Error type: {:?}", e);
|
|
error!(" Error message: {}", e);
|
|
warn!(
|
|
"⚠ ML Training service unavailable: {}. API Gateway will run without ML endpoints.",
|
|
e
|
|
);
|
|
warn!(" ML training endpoints will return 503 Service Unavailable");
|
|
None
|
|
},
|
|
};
|
|
|
|
// Initialize configuration manager (requires database)
|
|
let database_url = std::env::var("DATABASE_URL")
|
|
.map_err(|_| anyhow::anyhow!("DATABASE_URL environment variable must be set"))?;
|
|
let db_pool = sqlx::PgPool::connect(&database_url)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to connect to database: {}", e))?;
|
|
info!("✓ Database connection established");
|
|
|
|
// Create Redis connection for config manager
|
|
let redis_client = redis::Client::open(args.redis_url.clone())
|
|
.map_err(|e| anyhow::anyhow!("Failed to create Redis client: {}", e))?;
|
|
let redis_conn = redis::aio::ConnectionManager::new(redis_client)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to create Redis connection manager: {}", e))?;
|
|
|
|
let mut config_manager = api_gateway::ConfigurationManager::new(db_pool.clone(), redis_conn)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to create configuration manager: {}", e))?;
|
|
|
|
// Start NOTIFY listener for hot-reload
|
|
config_manager
|
|
.start_listening()
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to start configuration listener: {}", e))?;
|
|
info!("✓ Configuration manager initialized with hot-reload");
|
|
|
|
// Load TLS configuration if enabled (Wave H1 Security Enforcement)
|
|
let tls_enabled = std::env::var("TLS_ENABLED")
|
|
.unwrap_or_else(|_| "false".to_string())
|
|
.parse::<bool>()
|
|
.unwrap_or(false);
|
|
|
|
let tls_config = if tls_enabled {
|
|
info!("🔒 TLS/mTLS enabled - initializing TLS 1.3 configuration");
|
|
|
|
let cert_path = std::env::var("TLS_CERT_PATH")
|
|
.unwrap_or_else(|_| "./certs/server-cert.pem".to_string());
|
|
let key_path =
|
|
std::env::var("TLS_KEY_PATH").unwrap_or_else(|_| "./certs/server-key.pem".to_string());
|
|
let ca_path =
|
|
std::env::var("TLS_CA_PATH").unwrap_or_else(|_| "./certs/ca/ca-cert.pem".to_string());
|
|
let require_client_cert = std::env::var("TLS_REQUIRE_CLIENT_CERT")
|
|
.unwrap_or_else(|_| "true".to_string())
|
|
.parse::<bool>()
|
|
.unwrap_or(true);
|
|
let enable_revocation = std::env::var("MTLS_ENABLE_REVOCATION_CHECK")
|
|
.unwrap_or_else(|_| "false".to_string())
|
|
.parse::<bool>()
|
|
.unwrap_or(false);
|
|
let crl_url = std::env::var("MTLS_CRL_URL").ok();
|
|
|
|
let tls = api_gateway::auth::mtls::ApiGatewayTlsConfig::from_files(
|
|
&cert_path,
|
|
&key_path,
|
|
&ca_path,
|
|
require_client_cert,
|
|
enable_revocation,
|
|
crl_url,
|
|
)
|
|
.await?;
|
|
|
|
info!(
|
|
"✓ TLS configuration loaded - Protocol: TLS 1.3, mTLS: {}, Revocation: {}",
|
|
require_client_cert, enable_revocation
|
|
);
|
|
Some(tls)
|
|
} else {
|
|
info!("⚠ TLS disabled - running in development mode (set TLS_ENABLED=true for production)");
|
|
None
|
|
};
|
|
|
|
// Build gRPC server with all services
|
|
use api_gateway::foxhunt::tli::{
|
|
backtesting_service_server::BacktestingServiceServer,
|
|
trading_service_server::TradingServiceServer,
|
|
};
|
|
use api_gateway::ml_training::ml_training_service_server::MlTrainingServiceServer;
|
|
|
|
let addr: std::net::SocketAddr = args
|
|
.bind_addr
|
|
.parse()
|
|
.map_err(|e| anyhow::anyhow!("Failed to parse bind address '{}': {}", args.bind_addr, e))?;
|
|
|
|
info!("Starting gRPC server on {}", addr);
|
|
|
|
// Setup graceful shutdown
|
|
let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
|
|
|
// Spawn signal handler for graceful shutdown
|
|
tokio::spawn(async move {
|
|
match tokio::signal::ctrl_c().await {
|
|
Ok(()) => {
|
|
info!("Received shutdown signal, draining connections...");
|
|
}
|
|
Err(e) => {
|
|
error!("Failed to listen for ctrl-c signal: {}", e);
|
|
}
|
|
}
|
|
let _ = shutdown_tx.send(());
|
|
});
|
|
|
|
// Setup health check service
|
|
let (health_reporter, health_service) = tonic_health::server::health_reporter();
|
|
|
|
// Always register trading service (required)
|
|
health_reporter
|
|
.set_serving::<TradingServiceServer<api_gateway::grpc::TradingServiceProxy>>()
|
|
.await;
|
|
|
|
// Conditionally register optional services
|
|
if backtesting_proxy.is_some() {
|
|
health_reporter
|
|
.set_serving::<BacktestingServiceServer<api_gateway::grpc::BacktestingServiceProxy>>()
|
|
.await;
|
|
}
|
|
if ml_training_proxy.is_some() {
|
|
health_reporter
|
|
.set_serving::<MlTrainingServiceServer<api_gateway::grpc::MlTrainingProxy>>()
|
|
.await;
|
|
}
|
|
|
|
// Initialize and start Prometheus metrics HTTP endpoint on port 9091
|
|
let gateway_metrics = api_gateway::metrics::GatewayMetrics::new()
|
|
.map_err(|e| anyhow::anyhow!("Failed to initialize gateway metrics: {}", e))?;
|
|
|
|
// Add service info metric (always present)
|
|
use prometheus::{register_gauge_with_registry, Opts};
|
|
let service_info = register_gauge_with_registry!(
|
|
Opts::new(
|
|
"api_gateway_service_info",
|
|
"API Gateway service information"
|
|
)
|
|
.const_label("version", env!("CARGO_PKG_VERSION"))
|
|
.const_label("service", "api_gateway"),
|
|
gateway_metrics.registry().as_ref()
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("Failed to register service info: {}", e))?;
|
|
service_info.set(1.0);
|
|
|
|
let metrics_registry = gateway_metrics.registry();
|
|
|
|
tokio::spawn(async move {
|
|
// Use combined router for metrics AND health/resilience endpoints
|
|
let combined_app = api_gateway::metrics::combined_router(metrics_registry);
|
|
|
|
let metrics_addr = "0.0.0.0:9091";
|
|
info!(
|
|
"Prometheus metrics endpoint listening on http://{}",
|
|
metrics_addr
|
|
);
|
|
info!("Health endpoints available:");
|
|
info!(" - GET http://{}/health/liveness", metrics_addr);
|
|
info!(" - GET http://{}/health/readiness", metrics_addr);
|
|
info!(" - GET http://{}/health/startup", metrics_addr);
|
|
info!(
|
|
" - GET http://{}/resilience/circuit-breaker/status",
|
|
metrics_addr
|
|
);
|
|
info!(
|
|
" - GET http://{}/resilience/rate-limit/status",
|
|
metrics_addr
|
|
);
|
|
info!(" - GET http://{}/resilience/timeout/config", metrics_addr);
|
|
info!(" - GET http://{}/resilience/retry/config", metrics_addr);
|
|
|
|
let listener = match tokio::net::TcpListener::bind(metrics_addr).await {
|
|
Ok(l) => l,
|
|
Err(e) => {
|
|
error!("Failed to bind metrics endpoint on {}: {}", metrics_addr, e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Err(e) = axum::serve(listener, combined_app).await {
|
|
error!("Metrics server failed: {}", e);
|
|
}
|
|
});
|
|
|
|
// Initialize REST API server for ML inference endpoints (port 8080)
|
|
if let Some(_ml_proxy) = ml_training_proxy.as_ref() {
|
|
let ml_config_rest = api_gateway::grpc::MlTrainingBackendConfig {
|
|
address: ml_training_backend_url.clone(),
|
|
connect_timeout_ms: 5000,
|
|
request_timeout_ms: 30000,
|
|
circuit_breaker_failures: 5,
|
|
circuit_breaker_reset_secs: 30,
|
|
tls_ca_cert_path: ml_training_tls_ca_cert.clone(),
|
|
tls_client_cert_path: ml_training_tls_client_cert.clone(),
|
|
tls_client_key_path: ml_training_tls_client_key.clone(),
|
|
};
|
|
let ml_client = api_gateway::setup_ml_training_client(ml_config_rest)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!("Failed to setup ML training client for REST API: {}", e))?;
|
|
|
|
// Create Trading Service client for ML prediction proxying
|
|
let trading_channel = tonic::transport::Channel::from_shared(
|
|
trading_backend_url.clone(),
|
|
)
|
|
.map_err(|e| anyhow::anyhow!("Invalid TRADING_SERVICE_URL for ML REST proxy: {}", e))?
|
|
.connect_lazy();
|
|
let trading_client =
|
|
api_gateway::trading_backend::trading_service_client::TradingServiceClient::new(
|
|
trading_channel,
|
|
);
|
|
|
|
// Create ML handler state with auth components
|
|
let ml_handler_state = Arc::new(api_gateway::MlHandlerState {
|
|
ml_client,
|
|
trading_client,
|
|
auth: Arc::new(auth_interceptor.clone()),
|
|
rate_limiter: Arc::new(rate_limiter_rest),
|
|
});
|
|
|
|
// Create auth middleware state (for REST API)
|
|
let auth_middleware_state = Arc::new(api_gateway::AuthMiddlewareState {
|
|
jwt_service: Arc::new(jwt_service_rest),
|
|
revocation_service: Arc::new(revocation_service_rest),
|
|
rate_limiter: ml_handler_state.rate_limiter.clone(),
|
|
});
|
|
|
|
// Build ML REST API router with authentication middleware
|
|
use axum::middleware;
|
|
let ml_api_router = api_gateway::ml_router(ml_handler_state).layer(
|
|
middleware::from_fn_with_state(auth_middleware_state, api_gateway::jwt_auth_middleware),
|
|
);
|
|
|
|
// Spawn REST API server on port 8080
|
|
tokio::spawn(async move {
|
|
let rest_addr = "0.0.0.0:8080";
|
|
info!("REST API server listening on http://{}", rest_addr);
|
|
info!("ML inference endpoints available:");
|
|
info!(" - POST http://{}/api/v1/ml/predict", rest_addr);
|
|
info!(" - POST http://{}/api/v1/ml/batch_predict", rest_addr);
|
|
info!(" - GET http://{}/api/v1/ml/model_status", rest_addr);
|
|
info!(" - POST http://{}/api/v1/ml/hot_swap", rest_addr);
|
|
info!("Authentication: JWT Bearer token required (100 req/sec rate limit)");
|
|
|
|
let listener = match tokio::net::TcpListener::bind(rest_addr).await {
|
|
Ok(l) => l,
|
|
Err(e) => {
|
|
error!("Failed to bind REST API endpoint on {}: {}", rest_addr, e);
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Err(e) = axum::serve(listener, ml_api_router).await {
|
|
error!("REST API server failed: {}", e);
|
|
}
|
|
});
|
|
} else {
|
|
warn!("ML Training Service unavailable - REST API endpoints disabled");
|
|
}
|
|
|
|
// Build server with HTTP/2 optimizations
|
|
let mut server_builder = if let Some(ref tls) = tls_config {
|
|
tonic::transport::Server::builder()
|
|
.layer(GrpcMetricsLayer::new("api-gateway"))
|
|
.tls_config(tls.to_server_tls_config())?
|
|
.max_concurrent_streams(Some(10_000))
|
|
.http2_keepalive_interval(Some(Duration::from_secs(30)))
|
|
.http2_keepalive_timeout(Some(Duration::from_secs(10)))
|
|
} else {
|
|
tonic::transport::Server::builder()
|
|
.layer(GrpcMetricsLayer::new("api-gateway"))
|
|
.max_concurrent_streams(Some(10_000))
|
|
.http2_keepalive_interval(Some(Duration::from_secs(30)))
|
|
.http2_keepalive_timeout(Some(Duration::from_secs(10)))
|
|
}
|
|
.layer(tower::ServiceBuilder::new().layer(tower::layer::util::Identity::new())); // Placeholder for auth interceptor layer
|
|
|
|
// Add health service
|
|
let mut router = server_builder.add_service(health_service);
|
|
|
|
// Always add trading service (required) with authentication
|
|
router = router.add_service(TradingServiceServer::with_interceptor(
|
|
trading_proxy,
|
|
auth_interceptor.clone(),
|
|
));
|
|
|
|
// Track service availability for logging
|
|
let backtesting_available = backtesting_proxy.is_some();
|
|
let ml_training_available = ml_training_proxy.is_some();
|
|
|
|
// Conditionally add optional services with authentication
|
|
if let Some(backtesting) = backtesting_proxy.as_ref() {
|
|
// Clone the entire Arc - tonic services can work with Arc-wrapped implementations
|
|
router = router.add_service(BacktestingServiceServer::with_interceptor(
|
|
Arc::clone(backtesting),
|
|
auth_interceptor.clone(),
|
|
));
|
|
}
|
|
if let Some(ml_training) = ml_training_proxy {
|
|
router = router.add_service(MlTrainingServiceServer::with_interceptor(
|
|
ml_training,
|
|
auth_interceptor.clone(),
|
|
));
|
|
}
|
|
|
|
// Log startup information
|
|
info!("🚀 API Gateway listening on {}", addr);
|
|
info!(" - Trading Service: {} (REQUIRED)", trading_backend_url);
|
|
info!(
|
|
" - Backtesting Service: {} ({})",
|
|
backtesting_backend_url,
|
|
if backtesting_available {
|
|
"✓ AVAILABLE"
|
|
} else {
|
|
"✗ UNAVAILABLE"
|
|
}
|
|
);
|
|
info!(
|
|
" - ML Training Service: {} ({})",
|
|
ml_training_backend_url,
|
|
if ml_training_available {
|
|
"✓ AVAILABLE"
|
|
} else {
|
|
"✗ UNAVAILABLE"
|
|
}
|
|
);
|
|
info!(" - Health checks: enabled");
|
|
info!(" - Authentication: 6-layer (<10μs overhead)");
|
|
info!(" - Rate limiting: {} req/s per user", args.rate_limit_rps);
|
|
|
|
// Start server with graceful shutdown
|
|
let server = router.serve_with_shutdown(addr, async {
|
|
shutdown_rx.await.ok();
|
|
});
|
|
|
|
// Start server
|
|
server.await?;
|
|
|
|
info!("API Gateway shutdown complete");
|
|
Ok(())
|
|
}
|
|
|
|
/// Load JWT configuration from Vault (production) or environment (development)
|
|
///
|
|
/// Priority:
|
|
/// 1. Vault (secret/foxhunt/jwt) - Production
|
|
/// 2. JWT_SECRET_FILE - File-based secret
|
|
/// 3. JWT_SECRET env var - Development fallback
|
|
async fn load_jwt_config() -> Result<JwtConfig> {
|
|
JwtConfig::new().await
|
|
}
|