Add start server command and logging setup for CLI

This commit is contained in:
GW_MC
2025-12-05 18:03:17 +08:00
parent d2b842d933
commit 65d40c1e83
5 changed files with 192 additions and 163 deletions

View File

@@ -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<tracing_subscriber::fmt::format::Full, BoxedTimer>,
> {
// 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<sea_orm::DatabaseConnection>) -> 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<dyn tracing_subscriber::fmt::time::FormatTime + Send + Sync + 'static>);
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;
}