From aa3d1e82c81fe2d025910ebd20411c5e641104b6 Mon Sep 17 00:00:00 2001 From: Rom3dius Date: Tue, 26 Mar 2024 18:47:25 +0100 Subject: [PATCH] added toml config and clean shutdown for axum removed log file/stdout config, initialize logger in config added optional log file with more detailed logging added back dotenv support, hierarchical config loading --- .gitignore | 2 + Cargo.lock | 249 +++++++++++++++++++++++++++++++++++- kairos-server/.env | 2 + kairos-server/Cargo.toml | 4 +- kairos-server/README.md | 24 ++++ kairos-server/src/config.rs | 131 ++++++++++++++++--- kairos-server/src/main.rs | 45 +++++-- 7 files changed, 424 insertions(+), 33 deletions(-) create mode 100644 kairos-server/README.md diff --git a/.gitignore b/.gitignore index 37358052..04ea5e57 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ result target .tmp .nixos-test-history +kairos_config.toml +kairos.log \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e42fd1ad..5b98705e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -333,6 +333,9 @@ name = "bitflags" version = "2.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -631,6 +634,26 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "const-oid" version = "0.4.5" @@ -643,12 +666,41 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "const_panic" version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6051f239ecec86fde3410901ab7860d458d160371533842974fc61f96d15879b" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cookie" version = "0.18.0" @@ -888,6 +940,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1310,9 +1371,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.3" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" [[package]] name = "hashbrown" @@ -1658,6 +1719,17 @@ dependencies = [ "serde_json", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "jzon" version = "0.12.5" @@ -1708,7 +1780,9 @@ dependencies = [ "axum", "axum-extra", "axum-test", + "config", "dotenvy", + "lazy_static", "proptest", "serde", "serde_json", @@ -1801,6 +1875,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -2098,6 +2178,16 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "overload" version = "0.1.1" @@ -2127,6 +2217,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "pem" version = "0.8.3" @@ -2153,6 +2249,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.53", +] + +[[package]] +name = "pest_meta" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "pin-project" version = "1.1.5" @@ -2594,6 +2735,28 @@ dependencies = [ "subtle", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.4.2", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rust-multipart-rfc7578_2" version = "0.6.1" @@ -2828,6 +2991,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3133,6 +3305,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -3203,6 +3384,40 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -3320,6 +3535,12 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e36a83ea2b3c704935a01b4642946aadd445cea40b10935e3f8bd8052b8193d6" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "uint" version = "0.9.5" @@ -3368,6 +3589,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "untrusted" version = "0.7.1" @@ -3694,6 +3921,15 @@ version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" +[[package]] +name = "winnow" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" +dependencies = [ + "memchr", +] + [[package]] name = "winreg" version = "0.50.0" @@ -3719,6 +3955,15 @@ dependencies = [ "tap", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/kairos-server/.env b/kairos-server/.env index a857ae62..3e9cb3d8 100644 --- a/kairos-server/.env +++ b/kairos-server/.env @@ -1 +1,3 @@ KAIROS_SERVER_PORT="8000" +KAIROS_SERVER_ADDRESS="0.0.0.0" +KAIROS_LOG_LEVEL="trace" \ No newline at end of file diff --git a/kairos-server/Cargo.toml b/kairos-server/Cargo.toml index d390e02e..24acd56c 100644 --- a/kairos-server/Cargo.toml +++ b/kairos-server/Cargo.toml @@ -12,7 +12,6 @@ name = "kairos-server" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dotenvy = "0.15" axum = { version = "0.7", features = ["tracing"] } axum-extra = { version = "0.9", features = [ "typed-routing", @@ -25,6 +24,9 @@ serde_json = "1" tokio = { version = "1", features = ["full", "tracing", "macros"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["std", "env-filter"] } +lazy_static = "1.4.0" +config = "0.14.0" +dotenvy = "0.15.7" [dev-dependencies] proptest = "1" diff --git a/kairos-server/README.md b/kairos-server/README.md new file mode 100644 index 00000000..fb04053c --- /dev/null +++ b/kairos-server/README.md @@ -0,0 +1,24 @@ +# Kairos Server + +## Configuration + +You can configure this application through environment variables, dotenv or a toml file. Below you can see examples of the latter two. + +**dotenv** +``` +KAIROS_SERVER_PORT="8000" +KAIROS_SERVER_ADDRESS="0.0.0.0" +KAIROS_LOG_LEVEL="trace" # trace, debug, info, warn, error +KAIROS_LOG_FILE="kairos.log" # Optional +``` + +**kairos_config.toml** +``` +[server] +address = "0.0.0.0" +port = 8000 + +[log] +level = "trace" # trace, debug, info, warn, error +file = "kairos.log" # optional +``` \ No newline at end of file diff --git a/kairos-server/src/config.rs b/kairos-server/src/config.rs index a93752b7..0254c561 100644 --- a/kairos-server/src/config.rs +++ b/kairos-server/src/config.rs @@ -1,26 +1,119 @@ -use std::{fmt, str::FromStr}; +use config::{Config as Configuration, Environment, File}; +use dotenvy::dotenv; +use serde::{Deserialize, Serialize}; +use std::{fs::File as FsFile, net::SocketAddr, path::Path}; +use tracing::{subscriber::set_global_default, Level}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; -#[derive(Debug)] -pub struct ServerConfig { - pub port: u16, -} +fn serialize_level_filter(level: &Level, serializer: S) -> Result +where + S: serde::Serializer, +{ + let level_str = match *level { + Level::ERROR => "error", + Level::WARN => "warn", + Level::INFO => "info", + Level::DEBUG => "debug", + Level::TRACE => "trace", + }; -impl ServerConfig { - pub fn from_env() -> Result { - let port = parse_env_as::("KAIROS_SERVER_PORT")?; - Ok(Self { port }) - } + serializer.serialize_str(level_str) } -fn parse_env_as(env: &str) -> Result +fn deserialize_level_filter<'de, D>(deserializer: D) -> Result where - T: FromStr, - ::Err: fmt::Display, + D: serde::Deserializer<'de>, { - std::env::var(env) - .map_err(|e| format!("Failed to fetch environment variable {}: {}", env, e)) - .and_then(|val| { - val.parse::() - .map_err(|e| format!("Failed to parse {}: {}", env, e)) - }) + let s = String::deserialize(deserializer)?; + match s.to_lowercase().as_str() { + "error" => Ok(Level::ERROR), + "warn" => Ok(Level::WARN), + "info" => Ok(Level::INFO), + "debug" => Ok(Level::DEBUG), + "trace" => Ok(Level::TRACE), + _ => Err(serde::de::Error::custom("unknown level")), + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Server { + pub address: String, + pub port: u16, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Log { + #[serde( + deserialize_with = "deserialize_level_filter", + serialize_with = "serialize_level_filter" + )] + pub level: Level, + pub file: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Settings { + pub server: Server, + pub log: Log, +} + +#[allow(clippy::new_without_default)] +impl Settings { + pub fn new() -> Self { + dotenv().ok(); + + // Start building the configuration + let mut builder = Configuration::builder(); + + // Check if the kairos_config.toml file exists before adding it as a source + let config_path = "kairos_config.toml"; + if Path::new(config_path).exists() { + builder = builder.add_source(File::new(config_path, config::FileFormat::Toml)); + } + + // Add environment variables as a source + builder = builder.add_source(Environment::with_prefix("KAIROS").separator("_")); + match builder.build() { + Ok(config) => match config.try_deserialize::() { + Ok(settings) => settings, + Err(e) => { + eprintln!("Failed to deserialize config: {}", e); + std::process::exit(1); + } + }, + Err(e) => { + eprintln!("Failed to build config: {}", e); + std::process::exit(1); + } + } + } + + pub fn socket_address(&self) -> SocketAddr { + format!("{}:{}", self.server.address, self.server.port) + .parse() + .expect("Invalid address") + } + + pub fn initialize_logger(&self) { + let stdout_log = fmt::layer().pretty(); + let level_filter = EnvFilter::new(self.log.level.to_string()); + let subscriber = tracing_subscriber::Registry::default() + .with(stdout_log) + .with(level_filter); + + let file_log = self.log.file.as_ref().map(|file_name| { + let file = FsFile::create(file_name).expect("Failed to open log file."); + fmt::layer() + .with_file(true) + .with_line_number(true) + .with_writer(file) + }); + + if let Some(file_layer) = file_log { + set_global_default(subscriber.with(file_layer)) + .expect("Unable to set global logging configuration"); + } else { + set_global_default(subscriber).expect("Unable to set global logging configuration"); + } + } } diff --git a/kairos-server/src/main.rs b/kairos-server/src/main.rs index 0ea31571..7422dd9e 100644 --- a/kairos-server/src/main.rs +++ b/kairos-server/src/main.rs @@ -1,21 +1,44 @@ -use std::net::SocketAddr; +use lazy_static::lazy_static; -use dotenvy::dotenv; -use kairos_server::{config::ServerConfig, state::BatchState}; +use kairos_server::{config::Settings, state::BatchState}; + +// Globally accessible config +lazy_static! { + static ref CONFIG: Settings = Settings::new(); +} #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); - dotenv().ok(); - - let config = ServerConfig::from_env() - .unwrap_or_else(|e| panic!("Failed to parse server config from environment: {}", e)); + CONFIG.initialize_logger(); let app = kairos_server::app_router(BatchState::new()); - let axum_addr = SocketAddr::from(([127, 0, 0, 1], config.port)); + let axum_addr = CONFIG.socket_address(); - tracing::info!("starting http server on `{}`", axum_addr); + tracing::info!("Starting HTTP server on `{}`", axum_addr); let listener = tokio::net::TcpListener::bind(axum_addr).await.unwrap(); - axum::serve(listener, app).await.unwrap(); + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal()) + .await + .unwrap(); +} + +async fn shutdown_signal() { + let ctrl_c = async { + tokio::signal::ctrl_c() + .await + .expect("Failed to install Ctrl+C handler"); + }; + + let terminate = async { + tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) + .expect("Failed to install signal handler") + .recv() + .await; + }; + + tokio::select! { + _ = ctrl_c => {tracing::info!("Received CTRL+C signal, shutting down...")}, + _ = terminate => {tracing::info!("Received shutdown signal, shutting down...")}, + } }