From 4d8093c4a375a64cd8abd0b9df61f0b85e6dbf9c Mon Sep 17 00:00:00 2001 From: rm-dr <96270320+rm-dr@users.noreply.github.com> Date: Thu, 6 Nov 2025 20:45:44 -0800 Subject: [PATCH] Env config --- Cargo.lock | 19 ++++++ Cargo.toml | 2 + crates/bin/webpage/Cargo.toml | 1 + crates/bin/webpage/src/config.rs | 106 ++++++++++++++++++++++++++++++ crates/bin/webpage/src/main.rs | 42 ++++-------- crates/lib/page/src/server.rs | 2 +- crates/lib/toolbox/Cargo.toml | 4 ++ crates/lib/toolbox/src/env.rs | 106 ++++++++++++++++++++++++++++++ crates/lib/toolbox/src/lib.rs | 1 + crates/lib/toolbox/src/logging.rs | 20 +++++- 10 files changed, 272 insertions(+), 31 deletions(-) create mode 100644 crates/bin/webpage/src/config.rs create mode 100644 crates/lib/toolbox/src/env.rs diff --git a/Cargo.lock b/Cargo.lock index b961ffc..fb751f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -583,6 +583,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + [[package]] name = "downcast-rs" version = "1.2.1" @@ -619,6 +625,15 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca" +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -2631,8 +2646,11 @@ dependencies = [ "anyhow", "base64", "clap", + "dotenvy", + "envy", "num", "serde", + "thiserror", "tokio", "tracing", "tracing-loki", @@ -3067,6 +3085,7 @@ dependencies = [ "axum", "clap", "libservice", + "serde", "service-webpage", "tokio", "toolbox", diff --git a/Cargo.toml b/Cargo.toml index 1442fe5..fded44b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -117,6 +117,8 @@ tracing-loki = { version = "0.2.6", features = [ ], default-features = false } clap = { version = "4.5.51", features = ["derive"] } anstyle = { version = "1.0.13" } +envy = "0.4.2" +dotenvy = "0.15.7" # # MARK: Serialization & formats diff --git a/crates/bin/webpage/Cargo.toml b/crates/bin/webpage/Cargo.toml index 7f17f49..471bc19 100644 --- a/crates/bin/webpage/Cargo.toml +++ b/crates/bin/webpage/Cargo.toml @@ -17,3 +17,4 @@ tokio = { workspace = true } axum = { workspace = true } clap = { workspace = true } anyhow = { workspace = true } +serde = { workspace = true } diff --git a/crates/bin/webpage/src/config.rs b/crates/bin/webpage/src/config.rs new file mode 100644 index 0000000..3a24039 --- /dev/null +++ b/crates/bin/webpage/src/config.rs @@ -0,0 +1,106 @@ +use serde::Deserialize; +use std::num::NonZeroUsize; +use toolbox::{ + env::load_env, + logging::{LogFilterPreset, LoggingFormat, LoggingInitializer, LoggingTarget, LokiConfig}, +}; +use tracing::info; + +#[derive(Deserialize, Clone)] +pub struct WebpageConfig { + #[serde(default)] + pub loglevel: LogFilterPreset, + + #[serde(default)] + pub logformat: LoggingFormat, + + #[serde(flatten)] + pub loki: Option, + + // How many threads tokio should use + pub runtime_threads: Option, + pub blocking_threads: Option, +} + +impl WebpageConfig { + pub fn load() -> Self { + let config_res = match load_env::() { + Ok(x) => x, + + #[expect(clippy::print_stdout)] + Err(err) => { + println!("Error while loading .env: {err}"); + std::process::exit(1); + } + }; + + let config = config_res.get_config().clone(); + + info!(message = "Config loaded"); + + return config; + } + + /* + pub fn init_logging_noloki(&self) { + let res = LoggingInitializer { + app_name: "betalupi-webpage", + loki: None, + preset: self.loglevel, + target: LoggingTarget::Stderr { + format: self.logformat, + }, + } + .initialize(); + + if let Err(e) = res { + #[expect(clippy::print_stderr)] + for e in e.chain() { + eprintln!("{e}"); + } + + std::process::exit(1); + } + } + */ + + /// Must be run inside a tokio context, + /// use `init_logging_noloki` if you don't have async. + pub async fn init_logging(&self) { + let res = LoggingInitializer { + app_name: "betalupi-webpage", + loki: self.loki.clone(), + preset: self.loglevel, + target: LoggingTarget::Stderr { + format: self.logformat, + }, + } + .initialize(); + + if let Err(e) = res { + #[expect(clippy::print_stderr)] + for e in e.chain() { + eprintln!("{e}"); + } + + std::process::exit(1); + } + } + + pub fn make_runtime(&self) -> tokio::runtime::Runtime { + let mut rt = tokio::runtime::Builder::new_multi_thread(); + rt.enable_all(); + if let Some(threads) = self.runtime_threads { + rt.worker_threads(threads.into()); + } + + if let Some(threads) = self.blocking_threads { + rt.max_blocking_threads(threads.into()); + } + + #[expect(clippy::unwrap_used)] + let rt = rt.build().unwrap(); + + return rt; + } +} diff --git a/crates/bin/webpage/src/main.rs b/crates/bin/webpage/src/main.rs index dcd3afa..3c84b0b 100644 --- a/crates/bin/webpage/src/main.rs +++ b/crates/bin/webpage/src/main.rs @@ -1,10 +1,11 @@ use clap::Parser; -use toolbox::logging::{LogCliVQ, LoggingFormat, LoggingInitializer, LoggingTarget}; +use toolbox::logging::LogCliVQ; use tracing::error; -use crate::cmd::Command; +use crate::{cmd::Command, config::WebpageConfig}; mod cmd; +mod config; #[derive(Parser, Debug)] #[command(version, about, long_about = None, styles=toolbox::cli::clap_styles())] @@ -20,36 +21,19 @@ struct Cli { command: Command, } -#[derive(Debug)] -pub struct CmdContext {} +pub struct CmdContext { + config: WebpageConfig, +} -#[tokio::main] -async fn main() { +fn main() { let cli = Cli::parse(); + let ctx = CmdContext { + config: WebpageConfig::load(), + }; - { - let res = LoggingInitializer { - app_name: "webpage", - loki: None, - preset: cli.vq.into_preset(), - target: LoggingTarget::Stderr { - format: LoggingFormat::Ansi, - }, - } - .initialize(); - - if let Err(e) = res { - #[expect(clippy::print_stderr)] - for e in e.chain() { - eprintln!("{e}"); - } - - std::process::exit(1); - } - } - - let ctx = CmdContext {}; - let res = cli.command.run(ctx).await; + let rt = ctx.config.make_runtime(); + rt.block_on(ctx.config.init_logging()); + let res = rt.block_on(cli.command.run(ctx)); if let Err(e) = res { for e in e.chain() { diff --git a/crates/lib/page/src/server.rs b/crates/lib/page/src/server.rs index 425912f..304cc98 100644 --- a/crates/lib/page/src/server.rs +++ b/crates/lib/page/src/server.rs @@ -12,7 +12,7 @@ use maud::Markup; use parking_lot::Mutex; use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant}; use tower_http::compression::{CompressionLayer, DefaultPredicate}; -use tracing::{trace, warn}; +use tracing::trace; use crate::{ClientInfo, RequestContext, page::Page}; diff --git a/crates/lib/toolbox/Cargo.toml b/crates/lib/toolbox/Cargo.toml index fae188c..04a3c57 100644 --- a/crates/lib/toolbox/Cargo.toml +++ b/crates/lib/toolbox/Cargo.toml @@ -14,12 +14,16 @@ tokio = { workspace = true } serde = { workspace = true } tracing = { workspace = true } num = { workspace = true } +thiserror = { workspace = true } +envy = { workspace = true } +dotenvy = { workspace = true } clap = { workspace = true, optional = true } anstyle = { workspace = true, optional = true } tracing-subscriber = { workspace = true, optional = true } anyhow = { workspace = true, optional = true } + tracing-loki = { workspace = true, optional = true } tokio = { workspace = true, optional = true } url = { workspace = true, optional = true } diff --git a/crates/lib/toolbox/src/env.rs b/crates/lib/toolbox/src/env.rs new file mode 100644 index 0000000..13edf26 --- /dev/null +++ b/crates/lib/toolbox/src/env.rs @@ -0,0 +1,106 @@ +use serde::de::DeserializeOwned; +use std::{ + collections::HashMap, + env::VarError, + io::ErrorKind, + path::{Path, PathBuf}, +}; +use thiserror::Error; + +/// An error we might encounter when loading an env +#[derive(Debug, Error)] +pub enum EnvLoadError { + #[error("i/o error")] + IOError(#[from] std::io::Error), + + #[error("varerror")] + VarError(#[from] VarError), + + #[error("line parse error: `{on_line}` at char {at_char}")] + LineParse { on_line: String, at_char: usize }, + + #[error("other dotenvy error")] + Other(#[from] dotenvy::Error), + + #[error("missing value {0}")] + MissingValue(String), + + #[error("parse error: {0}")] + OtherParseError(String), +} + +pub enum LoadedEnv { + /// We loaded config from `.env` and env vars + FoundFile { config: T, path: PathBuf }, + + /// We could not find `.env` and only loaded env vars + OnlyVars(T), +} + +impl LoadedEnv { + pub fn get_config(&self) -> &T { + match self { + Self::FoundFile { config, .. } => config, + Self::OnlyVars(config) => config, + } + } +} + +/// Load the configuration type `T` from the current environment, +/// including the `.env` if it exists. +#[expect(clippy::wildcard_enum_match_arm)] +pub fn load_env() -> Result, EnvLoadError> { + let env_path = match dotenvy::dotenv() { + Ok(path) => Some(path), + + Err(dotenvy::Error::Io(err)) => match err.kind() { + ErrorKind::NotFound => None, + _ => return Err(EnvLoadError::IOError(err)), + }, + + Err(dotenvy::Error::EnvVar(err)) => { + return Err(EnvLoadError::VarError(err)); + } + + Err(dotenvy::Error::LineParse(on_line, at_char)) => { + return Err(EnvLoadError::LineParse { on_line, at_char }); + } + + Err(err) => { + return Err(EnvLoadError::Other(err)); + } + }; + + match envy::from_env::() { + Ok(config) => { + if let Some(path) = env_path { + return Ok(LoadedEnv::FoundFile { path, config }); + } else { + return Ok(LoadedEnv::OnlyVars(config)); + } + } + + Err(envy::Error::MissingValue(value)) => { + return Err(EnvLoadError::MissingValue(value.into())); + } + + Err(envy::Error::Custom(message)) => { + return Err(EnvLoadError::OtherParseError(message)); + } + }; +} + +/// Load an .env file to a hashmap. +/// +/// This function does not read the current env, +/// only parsing vars explicitly declared in the given file. +pub fn load_env_dict(p: impl AsRef) -> Result, EnvLoadError> { + let mut out = HashMap::new(); + + for item in dotenvy::from_filename_iter(p)? { + let (key, val) = item?; + out.insert(key, val); + } + + return Ok(out); +} diff --git a/crates/lib/toolbox/src/lib.rs b/crates/lib/toolbox/src/lib.rs index 7e803e4..036ddc5 100644 --- a/crates/lib/toolbox/src/lib.rs +++ b/crates/lib/toolbox/src/lib.rs @@ -1,5 +1,6 @@ //! This crate contains various bits of useful code that don't fit anywhere else. +pub mod env; pub mod mime; pub mod misc; pub mod strings; diff --git a/crates/lib/toolbox/src/logging.rs b/crates/lib/toolbox/src/logging.rs index 3429441..52f9cdc 100644 --- a/crates/lib/toolbox/src/logging.rs +++ b/crates/lib/toolbox/src/logging.rs @@ -120,8 +120,10 @@ impl LogCliVQ { preset = LogFilterPreset::Info } else if level_i == 1 { preset = LogFilterPreset::Debug - } else if level_i >= 2 { + } else if level_i == 2 { preset = LogFilterPreset::Trace + } else if level_i >= 3 { + preset = LogFilterPreset::HyperTrace } else { unreachable!() } @@ -153,6 +155,9 @@ pub enum LogFilterPreset { /// Standard "trace" log level Trace, + /// Trace EVERYTHING + HyperTrace, + /// Filter for loki subscriber. /// /// This is similar to `Trace`, @@ -240,6 +245,19 @@ impl LogFilterPreset { service: LogLevel::Trace, }, + Self::HyperTrace => LoggingConfig { + other: LogLevel::Trace, + silence: LogLevel::Trace, + + // Libs + libservice: LogLevel::Trace, + toolbox: LogLevel::Trace, + + // Bins + webpage: LogLevel::Trace, + service: LogLevel::Trace, + }, + Self::Loki => LoggingConfig { other: LogLevel::Trace, silence: LogLevel::Warn,