diff --git a/apps/api/src/cmd.rs b/apps/api/src/cmd.rs index ffb6eca..1475f26 100644 --- a/apps/api/src/cmd.rs +++ b/apps/api/src/cmd.rs @@ -1,22 +1,26 @@ +mod generate_openapi; +mod start_server; + +pub use start_server::start_server; + use std::pin::Pin; use std::{future::Future, process::exit}; use clap::{ArgMatches, Command}; -pub mod generate_openapi; - pub struct CliCommand { pub command: Command, pub action: fn(&clap::ArgMatches) -> Pin + Send>>, } static CLI_COMMANDS: once_cell::sync::Lazy< - [CliCommand; 1 /* Update this count when adding new commands */], + [CliCommand; 2 /* Update this count when adding new commands */], > = once_cell::sync::Lazy::new(|| { [ // Add new commands here generate_openapi::get_cli_command(), + start_server::get_cli_command(), ] }); diff --git a/apps/api/src/cmd/generate_openapi.rs b/apps/api/src/cmd/generate_openapi.rs index e227fed..8d0c6f6 100644 --- a/apps/api/src/cmd/generate_openapi.rs +++ b/apps/api/src/cmd/generate_openapi.rs @@ -1,7 +1,8 @@ use clap::{Arg, Command}; +use tracing::info; use utoipa::OpenApi; -use crate::{cmd::CliCommand, routes::ApiDoc}; +use crate::{cmd::CliCommand, log, routes::ApiDoc}; pub fn get_cli_command() -> CliCommand { CliCommand { @@ -28,11 +29,16 @@ fn action( ) -> std::pin::Pin + Send>> { let output_path = _matches.get_one::("output_path"); let output_path = output_path.unwrap().to_string(); + Box::pin(async move { - let doc = ApiDoc::openapi(); - let json = doc - .to_pretty_json() - .expect("Failed to serialize OpenAPI doc to JSON"); - std::fs::write(&output_path, json).expect("Failed to write OpenAPI doc to file"); + tracing::subscriber::with_default(log::make_temporary_subscriber(), || { + info!("Generating OpenAPI documentation..."); + let doc = ApiDoc::openapi(); + let json = doc + .to_pretty_json() + .expect("Failed to serialize OpenAPI doc to JSON"); + std::fs::write(&output_path, json).expect("Failed to write OpenAPI doc to file"); + info!("OpenAPI documentation generated at {}", output_path); + }) }) } diff --git a/apps/api/src/cmd/start_server.rs b/apps/api/src/cmd/start_server.rs new file mode 100644 index 0000000..461c0b3 --- /dev/null +++ b/apps/api/src/cmd/start_server.rs @@ -0,0 +1,142 @@ +use std::sync::Arc; + +use axum::Router; +use clap::Command; +use database::get_connection; +use sea_orm::ConnectOptions; +use tracing::{debug, info}; +use tracing_subscriber::fmt::format::{DefaultFields, Format}; + +use crate::{ + cmd::CliCommand, + configs::{ProgramSettings, get_program_settings, logging::LoggingSettings}, + log, + routes::{self, AppService, AppState}, + services::settings::SettingsService, + tasks, +}; + +pub fn get_cli_command() -> CliCommand { + CliCommand { + command: command(), + action, + } +} + +fn command() -> Command { + Command::new("start").about("Start the server") +} + +fn action( + _matches: &clap::ArgMatches, +) -> std::pin::Pin + Send>> { + Box::pin(async move {}) +} + +pub async fn start_server() { + let settings = tracing::subscriber::with_default( + log::make_temporary_subscriber(), + || -> ProgramSettings { + debug!("Temporary subscriber installed."); + info!("Reading configuration..."); + let settings = get_program_settings(); + info!("Configuration read successfully."); + debug!("Resetting global subscriber..."); + + let subscriber = get_global_tracing_subscriber_builder(&settings.logging).finish(); + tracing::subscriber::set_global_default(subscriber) + .expect("Failed to set global default subscriber"); + + debug!( + "Global subscriber set with logging level: {:?}", + settings.logging.level + ); + + settings + }, + ); + + tasks::startup::run_startup_tasks(&settings) + .await + .expect("Failed to run startup tasks"); + + // setup database connection pool + info!("Establishing database connection..."); + debug!("Database URL: {}", settings.database.url); + + let db_options = |options: &mut ConnectOptions| { + options.max_connections(settings.database.max_connections); + }; + + let db_connection = Arc::new( + get_connection(&settings.database.url, Some(db_options)) + .await + .expect("Failed to establish database connection"), + ); + + info!("Database connection established."); + + // build the axum app and run the server... + info!("Starting application..."); + let app: Router = routes::get_root_router(Arc::new(get_app_state(&db_connection))); + + let address = format!("{}:{}", settings.server.address, settings.server.port); + info!("Starting server at http://{}", address); + + let listener = tokio::net::TcpListener::bind(address) + .await + .expect("Failed to bind to address"); + + axum::serve(listener, app) + .await + .expect("Failed to run the server"); +} + +fn get_global_tracing_subscriber_builder( + settings: &LoggingSettings, +) -> tracing_subscriber::fmt::SubscriberBuilder< + DefaultFields, + Format, +> { + // After configuration is read, install the global subscriber + let builder = tracing_subscriber::fmt() + .with_max_level(settings.level) + .with_target(false) + .with_level(true); + + if settings.utc { + builder.with_timer(BoxedTimer(Box::new( + tracing_subscriber::fmt::time::UtcTime::rfc_3339(), + ))) + } else { + builder.with_timer(BoxedTimer(Box::new( + tracing_subscriber::fmt::time::ChronoLocal::rfc_3339(), + ))) + } +} + +fn get_app_state(db_connection: &Arc) -> AppState { + AppState { + database_connection: db_connection.clone(), + service: Arc::new(AppService { + settings: Arc::new(SettingsService::new(db_connection.clone())), + }), + } +} + +// A small wrapper that holds a boxed `FormatTime` trait object and itself +// implements `FormatTime`, allowing us to use it as a concrete type with +// `builder.with_timer` while still picking the concrete timer implementation +// at runtime. +// wrapper type to hold boxed timers and implement the `FormatTime` trait for +// a concrete type so `with_timer` may be called once outside the conditional. +struct BoxedTimer(Box); + +impl tracing_subscriber::fmt::time::FormatTime for BoxedTimer { + fn format_time( + &self, + w: &mut tracing_subscriber::fmt::format::Writer<'_>, + ) -> std::result::Result<(), std::fmt::Error> { + self.0.format_time(w) + } +} diff --git a/apps/api/src/log.rs b/apps/api/src/log.rs new file mode 100644 index 0000000..f38dd21 --- /dev/null +++ b/apps/api/src/log.rs @@ -0,0 +1,7 @@ +pub fn make_temporary_subscriber() -> tracing_subscriber::fmt::Subscriber { + tracing_subscriber::fmt() + .with_max_level(tracing::Level::DEBUG) + .with_target(false) + .with_level(true) + .finish() +} diff --git a/apps/api/src/main.rs b/apps/api/src/main.rs index f8a8ec9..ba0c82f 100644 --- a/apps/api/src/main.rs +++ b/apps/api/src/main.rs @@ -1,169 +1,39 @@ mod cmd; mod configs; mod errors; +mod log; mod middlewares; mod routes; mod services; mod tasks; -use std::sync::Arc; - -use axum::Router; -use database::get_connection; -use sea_orm::ConnectOptions; -use tracing::{debug, info}; -use tracing_subscriber::fmt::format::{DefaultFields, Format}; - -use crate::{ - configs::{ProgramSettings, get_program_settings, logging::LoggingSettings}, - routes::{AppService, AppState}, - services::settings::SettingsService, -}; - #[tokio::main] async fn main() { - // only run command line interface if arguments are provided + // If there are command-line arguments, treat it as a CLI command if std::env::args().len() > 1 { - process_commands().await; + tracing::subscriber::with_default(log::make_temporary_subscriber(), || { + use clap::error::ErrorKind; + // + let mut command = cmd::get_command(); + let help_output = format!("{}", command.render_help()); + let matches = command + .try_get_matches() + .unwrap_or_else(|err| match err.kind() { + ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { + err.print().expect("Error writing Error"); + std::process::exit(0); + } + _ => { + err.print().expect("Error writing Error"); + std::process::exit(1); + } + }); + cmd::execute(&matches, &help_output) + }) + .await; return; } - start_server().await; -} - -async fn process_commands() { - tracing::subscriber::with_default(make_temporary_subscriber(), async || { - use clap::error::ErrorKind; - - let mut command = cmd::get_command(); - let help_output = format!("{}", command.render_help()); - let matches = command - .try_get_matches() - .unwrap_or_else(|err| match err.kind() { - ErrorKind::DisplayHelp | ErrorKind::DisplayVersion => { - err.print().expect("Error writing Error"); - std::process::exit(0); - } - _ => { - err.print().expect("Error writing Error"); - std::process::exit(1); - } - }); - cmd::execute(&matches, &help_output).await; - }) - .await -} - -async fn start_server() { - let settings = - tracing::subscriber::with_default(make_temporary_subscriber(), || -> ProgramSettings { - debug!("Temporary subscriber installed."); - info!("Reading configuration..."); - let settings = get_program_settings(); - info!("Configuration read successfully."); - debug!("Resetting global subscriber..."); - - let subscriber = get_global_tracing_subscriber_builder(&settings.logging).finish(); - tracing::subscriber::set_global_default(subscriber) - .expect("Failed to set global default subscriber"); - - debug!( - "Global subscriber set with logging level: {:?}", - settings.logging.level - ); - - settings - }); - - tasks::startup::run_startup_tasks(&settings) - .await - .expect("Failed to run startup tasks"); - - // setup database connection pool - info!("Establishing database connection..."); - debug!("Database URL: {}", settings.database.url); - - let db_options = |options: &mut ConnectOptions| { - options.max_connections(settings.database.max_connections); - }; - - let db_connection = Arc::new( - get_connection(&settings.database.url, Some(db_options)) - .await - .expect("Failed to establish database connection"), - ); - - info!("Database connection established."); - - // build the axum app and run the server... - info!("Starting application..."); - let app: Router = routes::get_root_router(Arc::new(get_app_state(&db_connection))); - - let address = format!("{}:{}", settings.server.address, settings.server.port); - info!("Starting server at http://{}", address); - - let listener = tokio::net::TcpListener::bind(address) - .await - .expect("Failed to bind to address"); - - axum::serve(listener, app) - .await - .expect("Failed to run the server"); -} - -fn make_temporary_subscriber() -> tracing_subscriber::fmt::Subscriber { - tracing_subscriber::fmt() - .with_max_level(tracing::Level::DEBUG) - .with_target(false) - .with_level(true) - .finish() -} - -fn get_global_tracing_subscriber_builder( - settings: &LoggingSettings, -) -> tracing_subscriber::fmt::SubscriberBuilder< - DefaultFields, - Format, -> { - // After configuration is read, install the global subscriber - let builder = tracing_subscriber::fmt() - .with_max_level(settings.level) - .with_target(false) - .with_level(true); - - if settings.utc { - builder.with_timer(BoxedTimer(Box::new( - tracing_subscriber::fmt::time::UtcTime::rfc_3339(), - ))) - } else { - builder.with_timer(BoxedTimer(Box::new( - tracing_subscriber::fmt::time::ChronoLocal::rfc_3339(), - ))) - } -} - -fn get_app_state(db_connection: &Arc) -> AppState { - AppState { - database_connection: db_connection.clone(), - service: Arc::new(AppService { - settings: Arc::new(SettingsService::new(db_connection.clone())), - }), - } -} - -// A small wrapper that holds a boxed `FormatTime` trait object and itself -// implements `FormatTime`, allowing us to use it as a concrete type with -// `builder.with_timer` while still picking the concrete timer implementation -// at runtime. -// wrapper type to hold boxed timers and implement the `FormatTime` trait for -// a concrete type so `with_timer` may be called once outside the conditional. -struct BoxedTimer(Box); - -impl tracing_subscriber::fmt::time::FormatTime for BoxedTimer { - fn format_time( - &self, - w: &mut tracing_subscriber::fmt::format::Writer<'_>, - ) -> std::result::Result<(), std::fmt::Error> { - self.0.format_time(w) - } + // No command-line arguments, start the server normally + cmd::start_server().await; }