#[cfg(feature = "tls")]
mod tls;
use crate::{error::Result, extensions::DeserializeExt};
use config::{builder::DefaultState, ConfigBuilder, Environment, File, FileFormat};
use serde::{
de::{DeserializeOwned, Error},
Deserialize, Deserializer,
};
use serde_json::{from_value, Value};
use std::marker::PhantomData;
use std::{fmt::Debug, net::IpAddr};
const HOST_PTR: &str = "/host";
const PORT_PTR: &str = "/port";
#[cfg(feature = "tokio-metrics")]
const SERVER_METRICS_UPDATE_INTERVAL: &str = "/server/metrics/update_interval";
const LOG_LEVEL_PTR: &str = "/log/level";
const LOG_MSG_LENGTH: &str = "/log/msg/length";
const TRACE_LEVEL_PTR: &str = "/trace/level";
const SERVICE_NAME_PTR: &str = "/service/name";
const COMPONENT_NAME_PTR: &str = "/component/name";
const COMPONENT_VERSION_PTR: &str = "/component/version";
const TRACES_ENDPOINT_PTR: &str = "/exporter/otlp/traces/endpoint";
const DEFAULT_CONFIG: &str = include_str!("../resources/default_conf.toml");
const DEFAULT_SEPARATOR: &str = "_";
#[derive(Clone, Debug)]
pub enum ConfigSource<'a> {
String(&'a str, FileFormat),
File(&'a str),
EnvPrefix(&'a str),
}
#[derive(Deserialize, Debug, PartialEq, Eq, Copy, Clone)]
pub struct Empty {}
#[derive(Debug)]
pub struct AppConfig<T> {
pub host: IpAddr,
pub port: u16,
pub logger: LoggerConfig,
#[cfg(feature = "tls")]
pub tls: tls::TlsConfigurationVariables,
pub private: T,
}
#[derive(Debug)]
pub struct LoggerConfig {
pub log_level: String,
pub msg_length: Option<usize>,
pub trace_level: String,
pub service_name: String,
pub component_name: String,
pub version: String,
#[cfg(feature = "tokio-metrics")]
pub metrics_update_interval: std::time::Duration,
pub traces_endpoint: Option<String>,
}
impl<'de> Deserialize<'de> for LoggerConfig {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let mut config = Value::deserialize(deserializer)?;
let log_level = config.pointer_and_deserialize(LOG_LEVEL_PTR)?;
let trace_level = config.pointer_and_deserialize(TRACE_LEVEL_PTR)?;
let service_name = config.pointer_and_deserialize(SERVICE_NAME_PTR)?;
let component_name = config.pointer_and_deserialize(COMPONENT_NAME_PTR)?;
let version = config.pointer_and_deserialize(COMPONENT_VERSION_PTR)?;
let traces_endpoint = config
.pointer_mut(TRACES_ENDPOINT_PTR)
.map(Value::take)
.map(from_value::<String>)
.transpose()
.map_err(Error::custom)?;
#[cfg(feature = "tokio-metrics")]
let metrics_update_interval =
config.pointer_and_deserialize::<u64, D::Error>(SERVER_METRICS_UPDATE_INTERVAL)?;
let msg_length = config
.pointer_and_deserialize::<_, D::Error>(LOG_MSG_LENGTH)
.ok();
Ok(LoggerConfig {
log_level,
msg_length,
version,
trace_level,
service_name,
component_name,
traces_endpoint,
#[cfg(feature = "tokio-metrics")]
metrics_update_interval: std::time::Duration::from_millis(metrics_update_interval),
})
}
}
impl<'de, T> Deserialize<'de> for AppConfig<T>
where
T: Debug + DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let config = Value::deserialize(deserializer)?;
let host = config.pointer_and_deserialize(HOST_PTR)?;
let port = config.pointer_and_deserialize(PORT_PTR)?;
let logger = LoggerConfig::deserialize(&config).map_err(Error::custom)?;
#[cfg(feature = "tls")]
let tls = tls::TlsConfigurationVariables::deserialize(&config).map_err(Error::custom)?;
let private = T::deserialize(config).map_err(Error::custom)?;
Ok(AppConfig::<T> {
host,
port,
logger,
#[cfg(feature = "tls")]
tls,
private,
})
}
}
impl Default for AppConfig<Empty> {
#[allow(clippy::expect_used)]
fn default() -> Self {
AppConfig::builder()
.add_default()
.add_env_prefixed("OTEL")
.build()
.expect("Default config never fails")
}
}
impl<T> AppConfig<T> {
pub fn builder() -> AppConfigBuilder<T> {
AppConfigBuilder::new()
}
pub fn default_with(file_path: &str, env_prefix: &str) -> Result<Self>
where
T: Debug + DeserializeOwned,
{
AppConfig::builder()
.add_default()
.add_env_prefixed("OTEL")
.add_file(file_path)
.add_env_prefixed(env_prefix)
.build()
}
pub fn load_from<'a, S>(sources: S) -> Result<Self>
where
T: Debug + DeserializeOwned,
S: IntoIterator<Item = ConfigSource<'a>>,
{
let mut config_builder = AppConfig::<T>::builder()
.add_default()
.add_env_prefixed("OTEL");
for source in sources {
config_builder = match source {
ConfigSource::String(str, format) => config_builder.add_str(str, format),
ConfigSource::File(path) => config_builder.add_file(path),
ConfigSource::EnvPrefix(prefix) => config_builder.add_env_prefixed(prefix),
};
}
config_builder.build()
}
}
#[derive(Debug, Default)]
pub struct AppConfigBuilder<T> {
builder: ConfigBuilder<DefaultState>,
phantom: PhantomData<T>,
}
impl<T> AppConfigBuilder<T> {
pub fn new() -> Self {
Self {
builder: ConfigBuilder::default(),
phantom: PhantomData,
}
}
pub fn build(self) -> Result<AppConfig<T>>
where
T: Debug + DeserializeOwned,
{
Ok(self.builder.build()?.try_deserialize::<AppConfig<T>>()?)
}
pub fn add_default(mut self) -> Self {
self.builder = self
.builder
.add_source(File::from_str(DEFAULT_CONFIG, FileFormat::Toml));
self
}
pub fn add_file(mut self, path: &str) -> Self {
self.builder = self.builder.add_source(File::with_name(path));
self
}
pub fn add_str(mut self, str: &str, format: FileFormat) -> Self {
self.builder = self.builder.add_source(File::from_str(str, format));
self
}
pub fn add_env_prefixed(mut self, prefix: &str) -> Self {
self.builder = self.builder.add_source(
Environment::with_prefix(prefix)
.try_parsing(true)
.separator(DEFAULT_SEPARATOR),
);
self
}
}