use std::path::{Path, PathBuf};
use crate::config;
use crate::logging;
use crate::logging::settings::Settings;
#[non_exhaustive]
pub struct Bootstrap {
pub config: config::Store,
pub logging: logging::Handle,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum InitError {
#[error("could not load configuration: {0}")]
Config(#[from] config::Error),
#[error("could not initialize logging: {0}")]
Logging(#[from] logging::Error),
}
#[non_exhaustive]
pub struct Options {
pub service_name: String,
pub config_file: PathBuf,
pub dotenv: bool,
pub logging_key: String,
pub env_prefix: String,
}
impl Options {
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
config_file: PathBuf::from("config.toml"),
dotenv: true,
logging_key: "logging".to_string(),
env_prefix: String::new(),
}
}
}
pub async fn init(service_name: impl Into<String>) -> Result<Bootstrap, InitError> {
init_with(Options::new(service_name)).await
}
pub async fn init_with(options: Options) -> Result<Bootstrap, InitError> {
let store = build_store(&options).await?;
let settings = match Settings::read(&store, &options.logging_key) {
Ok(settings) => Some(settings),
Err(config::Error::NotFound(_)) => None,
Err(e) => return Err(InitError::Logging(logging::Error::Settings(e))),
};
let builder = logging_builder(&settings, &store)?;
let builder = apply_store_level(builder, &settings, &store)?;
let builder = auto_app_insights(builder, &settings, &store, &options.service_name);
let handle = builder.init()?;
tracing::info!(
service = %options.service_name,
config_file = %describe_file(&options.config_file),
dotenv = options.dotenv,
logging_key = %options.logging_key,
"configuration loaded, logging installed"
);
Ok(Bootstrap {
config: store,
logging: handle,
})
}
fn logging_builder(
settings: &Option<Settings>,
store: &config::Store,
) -> Result<logging::Builder, InitError> {
Ok(match settings {
Some(settings) => settings.apply(store)?,
None => logging::builder().console(logging::ConsoleConfig::default()),
})
}
fn apply_store_level(
builder: logging::Builder,
settings: &Option<Settings>,
store: &config::Store,
) -> Result<logging::Builder, InitError> {
if settings.as_ref().and_then(|s| s.level.as_deref()).is_some() {
return Ok(builder);
}
let Some(directives) = store.get_str("rust_log") else {
return Ok(builder);
};
let filter = logging::EnvFilter::builder()
.parse(&directives)
.map_err(|e| {
InitError::Logging(logging::Error::InvalidSettings(format!("rust_log: {e}")))
})?;
Ok(builder.with_filter(filter))
}
#[cfg(feature = "appinsights")]
fn auto_app_insights(
builder: logging::Builder,
settings: &Option<Settings>,
store: &config::Store,
service_name: &str,
) -> logging::Builder {
if settings.as_ref().is_some_and(|s| s.app_insights.is_some()) {
return builder;
}
let lookup = |name: &str| store.get_str(&name.to_lowercase());
match logging::appinsights::AppInsightsConfig::from_lookup(service_name, lookup) {
Ok(config) => builder.app_insights(config),
Err(_) => builder,
}
}
#[cfg(not(feature = "appinsights"))]
fn auto_app_insights(
builder: logging::Builder,
_settings: &Option<Settings>,
_store: &config::Store,
_service_name: &str,
) -> logging::Builder {
builder
}
async fn build_store(options: &Options) -> Result<config::Store, config::Error> {
const FILE_PRIORITY: u32 = 100;
const ENV_PRIORITY: u32 = 50;
const DOTENV_PRIORITY: u32 = 10;
let mut builder = config::Builder::default().env(&options.env_prefix, "__", ENV_PRIORITY);
if options.config_file.exists() {
builder = builder.toml(&options.config_file, FILE_PRIORITY);
}
if options.dotenv && Path::new(".env").exists() {
builder = builder.dotenv(".env", "", "__", DOTENV_PRIORITY)?;
}
builder.build().await
}
fn describe_file(path: &Path) -> String {
if path.exists() {
path.display().to_string()
} else {
format!("{} (absent, skipped)", path.display())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn options_default_to_the_conventional_stack() {
let options = Options::new("svc");
assert_eq!(options.service_name, "svc");
assert_eq!(options.config_file, PathBuf::from("config.toml"));
assert!(options.dotenv);
assert_eq!(options.logging_key, "logging");
assert_eq!(options.env_prefix, "");
}
#[test]
fn describe_file_marks_an_absent_path() {
let path = PathBuf::from("definitely-not-here-9f3a.toml");
let rendered = describe_file(&path);
assert!(rendered.contains("absent"), "got: {rendered}");
}
#[tokio::test]
async fn init_with_reports_an_invalid_logging_block() {
let file = crate::config::source::test_helpers::write_temp("[logging]\nlevel = 3\n");
let mut options = Options::new("svc");
options.config_file = file.path().to_path_buf();
options.dotenv = false;
let result = init_with(options).await;
assert!(matches!(result, Err(InitError::Logging(_))));
}
}