Env config
Some checks failed
CI / Check typos (push) Successful in 1m17s
CI / Check links (push) Failing after 1m15s
CI / Clippy (push) Successful in 2m19s
CI / Build and test (push) Successful in 2m1s
CI / Build container (push) Successful in 2m2s
CI / Deploy on waypoint (push) Successful in 47s
Some checks failed
CI / Check typos (push) Successful in 1m17s
CI / Check links (push) Failing after 1m15s
CI / Clippy (push) Successful in 2m19s
CI / Build and test (push) Successful in 2m1s
CI / Build container (push) Successful in 2m2s
CI / Deploy on waypoint (push) Successful in 47s
This commit is contained in:
19
Cargo.lock
generated
19
Cargo.lock
generated
@@ -583,6 +583,12 @@ dependencies = [
|
|||||||
"litrs",
|
"litrs",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dotenvy"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "downcast-rs"
|
name = "downcast-rs"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -619,6 +625,15 @@ version = "1.0.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
|
checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "envy"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -2631,8 +2646,11 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"base64",
|
"base64",
|
||||||
"clap",
|
"clap",
|
||||||
|
"dotenvy",
|
||||||
|
"envy",
|
||||||
"num",
|
"num",
|
||||||
"serde",
|
"serde",
|
||||||
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-loki",
|
"tracing-loki",
|
||||||
@@ -3067,6 +3085,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"clap",
|
"clap",
|
||||||
"libservice",
|
"libservice",
|
||||||
|
"serde",
|
||||||
"service-webpage",
|
"service-webpage",
|
||||||
"tokio",
|
"tokio",
|
||||||
"toolbox",
|
"toolbox",
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ tracing-loki = { version = "0.2.6", features = [
|
|||||||
], default-features = false }
|
], default-features = false }
|
||||||
clap = { version = "4.5.51", features = ["derive"] }
|
clap = { version = "4.5.51", features = ["derive"] }
|
||||||
anstyle = { version = "1.0.13" }
|
anstyle = { version = "1.0.13" }
|
||||||
|
envy = "0.4.2"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
|
||||||
#
|
#
|
||||||
# MARK: Serialization & formats
|
# MARK: Serialization & formats
|
||||||
|
|||||||
@@ -17,3 +17,4 @@ tokio = { workspace = true }
|
|||||||
axum = { workspace = true }
|
axum = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
serde = { workspace = true }
|
||||||
|
|||||||
106
crates/bin/webpage/src/config.rs
Normal file
106
crates/bin/webpage/src/config.rs
Normal file
@@ -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<LokiConfig>,
|
||||||
|
|
||||||
|
// How many threads tokio should use
|
||||||
|
pub runtime_threads: Option<NonZeroUsize>,
|
||||||
|
pub blocking_threads: Option<NonZeroUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WebpageConfig {
|
||||||
|
pub fn load() -> Self {
|
||||||
|
let config_res = match load_env::<WebpageConfig>() {
|
||||||
|
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: "shopkeep",
|
||||||
|
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: "Shopkeep",
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use toolbox::logging::{LogCliVQ, LoggingFormat, LoggingInitializer, LoggingTarget};
|
use toolbox::logging::LogCliVQ;
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
use crate::cmd::Command;
|
use crate::{cmd::Command, config::WebpageConfig};
|
||||||
|
|
||||||
mod cmd;
|
mod cmd;
|
||||||
|
mod config;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None, styles=toolbox::cli::clap_styles())]
|
#[command(version, about, long_about = None, styles=toolbox::cli::clap_styles())]
|
||||||
@@ -20,36 +21,19 @@ struct Cli {
|
|||||||
command: Command,
|
command: Command,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
pub struct CmdContext {
|
||||||
pub struct CmdContext {}
|
config: WebpageConfig,
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
fn main() {
|
||||||
async fn main() {
|
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
let ctx = CmdContext {
|
||||||
|
config: WebpageConfig::load(),
|
||||||
|
};
|
||||||
|
|
||||||
{
|
let rt = ctx.config.make_runtime();
|
||||||
let res = LoggingInitializer {
|
rt.block_on(ctx.config.init_logging());
|
||||||
app_name: "webpage",
|
let res = rt.block_on(cli.command.run(ctx));
|
||||||
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;
|
|
||||||
|
|
||||||
if let Err(e) = res {
|
if let Err(e) = res {
|
||||||
for e in e.chain() {
|
for e in e.chain() {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use maud::Markup;
|
|||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
|
use std::{collections::HashMap, num::NonZero, pin::Pin, sync::Arc, time::Instant};
|
||||||
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
use tower_http::compression::{CompressionLayer, DefaultPredicate};
|
||||||
use tracing::{trace, warn};
|
use tracing::trace;
|
||||||
|
|
||||||
use crate::{ClientInfo, RequestContext, page::Page};
|
use crate::{ClientInfo, RequestContext, page::Page};
|
||||||
|
|
||||||
|
|||||||
@@ -14,12 +14,16 @@ tokio = { workspace = true }
|
|||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
num = { workspace = true }
|
num = { workspace = true }
|
||||||
|
thiserror = { workspace = true }
|
||||||
|
envy = { workspace = true }
|
||||||
|
dotenvy = { workspace = true }
|
||||||
|
|
||||||
clap = { workspace = true, optional = true }
|
clap = { workspace = true, optional = true }
|
||||||
anstyle = { workspace = true, optional = true }
|
anstyle = { workspace = true, optional = true }
|
||||||
tracing-subscriber = { workspace = true, optional = true }
|
tracing-subscriber = { workspace = true, optional = true }
|
||||||
anyhow = { workspace = true, optional = true }
|
anyhow = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
|
||||||
tracing-loki = { workspace = true, optional = true }
|
tracing-loki = { workspace = true, optional = true }
|
||||||
tokio = { workspace = true, optional = true }
|
tokio = { workspace = true, optional = true }
|
||||||
url = { workspace = true, optional = true }
|
url = { workspace = true, optional = true }
|
||||||
|
|||||||
106
crates/lib/toolbox/src/env.rs
Normal file
106
crates/lib/toolbox/src/env.rs
Normal file
@@ -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<T> {
|
||||||
|
/// 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<T> LoadedEnv<T> {
|
||||||
|
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<T: DeserializeOwned>() -> Result<LoadedEnv<T>, 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::<T>() {
|
||||||
|
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<Path>) -> Result<HashMap<String, String>, 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);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! This crate contains various bits of useful code that don't fit anywhere else.
|
//! This crate contains various bits of useful code that don't fit anywhere else.
|
||||||
|
|
||||||
|
pub mod env;
|
||||||
pub mod mime;
|
pub mod mime;
|
||||||
pub mod misc;
|
pub mod misc;
|
||||||
pub mod strings;
|
pub mod strings;
|
||||||
|
|||||||
Reference in New Issue
Block a user