use crate::http::health::{HealthApiDoc, configure_server_base};
use crate::metrics::SysInfoCollector;
use crate::settings::Settings;
use actix_cors::Cors;
use actix_web::middleware::Condition;
use actix_web::web::ServiceConfig;
use actix_web::{App, HttpServer, middleware::Logger};
use actix_web_prom::{PrometheusMetrics, PrometheusMetricsBuilder};
use colored::Colorize;
use compose_rs::{Compose, ComposeCommand};
use prometheus::Registry;
use prometheus::process_collector::ProcessCollector;
use thiserror::Error;
use utoipa::OpenApi;
use tokio::join;
use utoipa_swagger_ui::{Config, SwaggerUi};
pub(crate) async fn bootstrap_server(
settings: &Settings,
fnconfig: Option<fn(&mut ServiceConfig)>,
) -> Result<Option<Compose>> {
let server_config = settings
.server
.as_ref()
.ok_or_else(|| HttpServerError::Configuration("Missing server configuration.".into()))?;
let num_threads = std::thread::available_parallelism().map_or_else(|_| 1, |p| p.get());
let compose = if server_config.use_docker_compose.unwrap_or(false) {
Some(run_docker_compose(settings).map_err(|_| HttpServerError::Compose)?)
} else {
None
};
let host = server_config
.host
.clone()
.ok_or_else(|| HttpServerError::Configuration("Missing server host.".into()))?;
let (health_metrics_enabled, prometheus_health) = configure_prometheus(settings, true)?;
let (metrics_enabled, prometheus) = configure_prometheus(settings, false)?;
let server_settings = settings.clone();
let mut main_server_builder = HttpServer::new(move || {
let cors_config = configure_cors(&server_settings);
let metrics_condition = Condition::new(metrics_enabled, prometheus.clone());
App::new()
.wrap(cors_config)
.wrap(metrics_condition)
.wrap(Logger::default())
.configure(fnconfig.unwrap_or(|_| {}))
})
.bind((host.clone(), server_config.port))
.map_err(|e| HttpServerError::Bootstrap(e.to_string()))?
.workers(server_config.workers.unwrap_or(num_threads))
.shutdown_timeout(60);
let mut health_server_builder = HttpServer::new(move || {
let health_openapi = HealthApiDoc::openapi();
let metrics_condition = Condition::new(health_metrics_enabled, prometheus_health.clone());
App::new()
.wrap(metrics_condition)
.configure(configure_server_base)
.service(
SwaggerUi::new("/actuator/swagger-ui/{_:.*}")
.url("/actuator/api-docs/openapi.json", health_openapi)
.config(Config::default().validator_url("none")),
)
})
.bind((host, server_config.health_check_port))
.map_err(|e| HttpServerError::Bootstrap(e.to_string()))?
.workers(server_config.health_check_workers.unwrap_or(num_threads))
.shutdown_timeout(60);
if let Some(workers) = settings.server.as_ref().and_then(|s| s.workers) {
main_server_builder = main_server_builder.workers(workers);
health_server_builder = health_server_builder.workers(workers);
}
let main_server = main_server_builder.run();
let health_server = health_server_builder.run();
tracing::info!(
"{} {}. {} {}.",
"Server listening on port".bright_green(),
server_config.port.to_string().bright_blue(),
"The Health Check port is".bright_green(),
server_config.health_check_port.to_string().bright_blue()
);
let (_, _) = join!(health_server, main_server);
Ok(compose)
}
fn configure_prometheus(settings: &Settings, base: bool) -> Result<(bool, PrometheusMetrics)> {
let metrics_cfg = settings.metrics.as_ref();
let metrics_enabled = metrics_cfg.and_then(|m| m.enabled).unwrap_or(false);
let metrics_app_name = metrics_cfg
.and_then(|m| m.app_name.clone())
.unwrap_or_else(|| "api".to_string());
let registry = build_metrics_registry(&metrics_app_name)?;
let endpoint = if base {
"/actuator/metrics"
} else {
"/metrics"
};
let prometheus = PrometheusMetricsBuilder::new(&metrics_app_name)
.endpoint(endpoint)
.exclude_regex("^/swagger-ui/.*")
.exclude_regex("^/actuator/swagger-ui/.*")
.registry(registry)
.build()
.map_err(|e| HttpServerError::Bootstrap(e.to_string()))?;
Ok((metrics_enabled, prometheus))
}
fn build_metrics_registry(app_name: &str) -> Result<Registry> {
let pid = std::process::id() as i32;
let registry = Registry::default();
registry
.register(Box::new(ProcessCollector::new(pid, app_name.to_string())))
.map_err(|e| HttpServerError::Configuration(e.to_string()))?;
let collector = SysInfoCollector::with_process_and_namespace(pid, app_name.to_string())
.map_err(|e| HttpServerError::Configuration(e.to_string()))?;
registry
.register(Box::new(collector))
.map_err(|e| HttpServerError::Configuration(e.to_string()))?;
Ok(registry)
}
fn run_docker_compose(settings: &Settings) -> Result<Compose> {
let server_config = settings
.server
.as_ref()
.ok_or_else(|| HttpServerError::Configuration("Server configuration is missing".into()))?;
let compose_file = server_config.docker_compose_file.as_ref().ok_or_else(|| {
HttpServerError::Configuration("Docker Compose file not configured".into())
})?;
let compose = Compose::builder()
.path(compose_file)
.build()
.map_err(|e| HttpServerError::Custom(format!("Failed to build Docker Compose: {e}")))?;
tracing::info!(
"{} {}. {}",
"Starting the docker compose from".to_string().green(),
compose_file.to_string().bright_blue(),
"Please wait...".to_string().green(),
);
if let Err(error) = compose.up().exec() {
tracing::error!("Error starting Docker Compose: {error}");
if let Err(down_error) = compose.down().exec() {
tracing::warn!("Error while stopping Docker Compose after failure: {down_error}");
}
return Err(HttpServerError::Custom(format!(
"Docker Compose startup failed: {error}"
)));
}
log_compose_status(&compose);
tracing::info!(
"{}",
"The Docker Compose containers are running! Starting the server...".green()
);
Ok(compose)
}
fn log_compose_status(compose: &Compose) {
match compose.ps().exec() {
Ok(services) => {
tracing::info!("{}", "Containers:".bright_green());
for service in services {
let status = format!("{:?}", service.status.status);
tracing::info!(
" {} {:<25} {} {:?}, {} {}{}",
"Name:".white(),
service.name.bright_blue(),
"Status:".white().dimmed(),
service.status.status,
"Since:".white().dimmed(),
service.status.since.bright_blue().dimmed(),
service
.status
.exit_code
.filter(|_| status == "Exited")
.map(|code| {
format!(
"{} {}",
", Exit Code:".bright_blue().dimmed(),
code.to_string().bright_blue().dimmed()
)
})
.unwrap_or_default()
);
}
}
Err(error) => {
tracing::warn!("Failed to retrieve Docker Compose status: {error}");
}
}
}
fn configure_cors(settings: &Settings) -> Cors {
if let Some(cors_config) = settings.server.as_ref().and_then(|sc| sc.cors.as_ref()) {
let mut cors = Cors::default();
if let Some(pattern) = &cors_config.allowed_origins_pattern {
let origins = pattern.split(',').collect::<Vec<&str>>();
if origins.len() == 1 && origins[0].trim() == "*" {
cors = cors.allow_any_origin();
} else {
for origin in origins {
cors = cors.allowed_origin(origin.trim());
}
}
};
if let Some(allowed_headers) = &cors_config.allowed_headers {
let headers = allowed_headers.split(',').collect::<Vec<&str>>();
if headers.len() == 1 && headers[0].trim() == "*" {
cors = cors.allow_any_header()
} else {
for header in headers {
cors = cors.allowed_header(header.trim());
}
}
}
cors
} else {
Cors::permissive()
}
}
pub struct ServerWrappers {
pub metrics_enabled: bool,
pub prometheus: PrometheusMetrics,
pub cors: Cors,
}
pub fn create_server_wrappers(settings: &Settings) -> Result<ServerWrappers> {
let (metrics_enabled, prometheus) = configure_prometheus(settings, false)?;
let cors = configure_cors(settings);
Ok(ServerWrappers {
metrics_enabled,
prometheus,
cors,
})
}
pub type Result<T, E = HttpServerError> = std::result::Result<T, E>;
#[derive(Debug, Error)]
pub enum HttpServerError {
#[error("Invalid HTTP server configuration: {0}")]
Configuration(String),
#[error("{0}")]
Custom(String),
#[error("Error on Docker Compose.")]
Compose,
#[error("Error initializing the HTTP server: {0}")]
Bootstrap(String),
}