lacoctelera/configuration.rs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
// Copyright 2024 Felipe Torres González
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//! La Coctelera configuration module.
//!
//! # Description
//!
//! This module includes all the definitions for the app's settings and the
//! objects that automate reading the configuration from files or environment
//! variables and parsing them to Rust's native types.
//!
//! Some settings must be overridden by environment variables.
//! All the environment variables that are meant to be used within this module
//! shall use the prefix `LACOCTELERA`.
//!
//! # Settings
//!
//! The settings of the application may be set via 2 methods:
//! - Using the configuration files located in the `config` folder.
//! - Using environment variables.
//!
//! The former is advised for settings that usually take the same values and don't include
//! any value that shall not be exposed to the public (passwords, tokens, ...).
//! The latter is advised for settings that we only intend to set for a limited amount of
//! time, i.e. a debug session, or contain private values.
//!
//! ## Environment Variables
//!
//! The following environment variables are accepted by the application:
//!
//! - `RUN_MODE`: `devel`, `prod`. This variable shall take a value that refers to a
//! configuration file in the `config` folder. The settings found there will
//! overridden the settings found in `base.toml`. When not set, `prod` is considered
//! as run mode.
//!
//! Variables defined within configuration files can be overridden using `LACOCTELERA`
//! prefix. Variables need to be scoped in the same way as they are found in the configuration
//! files. For example, to override [LogSettings::tracing_level]:
//!
//! ```bash
//! $ LACOCTELERA__APPLICATION__TRACING_LEVEL=trace ./lacoctelera
//! ```
//!
//! **Note that the scope separator is a double `_`.**
//!
//! When multiple configuration variables are needed to be overridden, it is advised to
//! create a `local.toml` file within the `config` folder.
//!
//! ## Configuration Files
//!
//! All the required configuration variables are found in: `base.toml`, `prod.toml`
//! and `devel.toml`. The former refers to common settings that are usually applied
//! to both running scenarios: production and development. Any variable found there can
//! be overridden if defined on any of the latter files.
//!
//! The descriptions for each variable are found in the `Struct`s docs:
//! - [ApplicationSettings] for settings that apply to the main application.
//! - [DataBaseSettings] for settings that apply to the DB connection.
use config::{Config, ConfigError, Environment, File};
use core::time;
use secrecy::{ExposeSecret, SecretString};
use serde_aux::field_attributes::deserialize_number_from_string;
use serde_derive::Deserialize;
use sqlx::mysql::{MySqlConnectOptions, MySqlSslMode};
use std::env;
use std::time::Duration;
use tracing::level_filters::LevelFilter;
/// Name of the directory in which configuration files will be stored.
const CONF_DIR: &str = "config";
/// Top level `struct` for the configuration.
#[derive(Clone, Debug, Deserialize)]
pub struct Settings {
pub application: ApplicationSettings,
/// DB Settings.
pub database: DataBaseSettings,
/// email client settings.
pub email_client: EmailClientSettings,
}
/// Application's settings.
#[derive(Clone, Debug, Deserialize)]
pub struct ApplicationSettings {
/// Listening port for the application.
#[serde(deserialize_with = "deserialize_number_from_string")]
pub port: u16,
/// Host address for the application.
pub host: String,
/// Base URL for accessing the application through the network.
pub base_url: String,
/// Log settings.
pub log_settings: LogSettings,
/// Number of maximum workers for the Tokio runtime
pub max_workers: u16,
}
/// Data Base connection settings.
#[derive(Clone, Debug, Deserialize)]
pub struct DataBaseSettings {
/// Host address for the DB server.
pub host: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
/// Listening port for the DB server.
pub port: u16,
/// Username to access the application's database.
pub username: String,
/// Password to access the application's database.
pub password: SecretString,
/// Name of the application's database.
pub db_name: String,
#[serde(deserialize_with = "deserialize_number_from_string")]
/// Maximum number of connections for the connections pool.
pub max_connections: u16,
/// Idle timeout for open connections.
#[serde(deserialize_with = "deserialize_number_from_string")]
pub idle_timeout_sec: u16,
/// Force using SSL for the connection to the DB. False sets the connection to `Preferred` mode.
pub require_ssl: bool,
}
/// Log related settings.
///
/// # Description
///
/// This application outputs logs to a file by default. The file's name and path is
/// set via [LogSettings::log_output_file]. Output messages are append to the file if it exists
/// previously. Use **logrotate** or any other application to avoid ending with an
/// enormous log file.
///
/// Aside from that file, the application allows to output log messages to _stdout_
/// as well, useful for debugging sessions. This feature is enabled via
/// [LogSettings::enable_console_log].
///
/// Finally, the severity of the log messages to the console (when enabled) is also
/// configurable, thus it is allowed to set different severity levels for the regular
/// file log and the console log. This might be useful to avoid cluttering the console
/// output with too much information that could be read from the logfile.
#[derive(Clone, Debug, Deserialize)]
pub struct LogSettings {
/// See [tracing::Level](https://docs.rs/tracing/0.1.40/tracing/struct.Level.html).
/// Accepted values are specified at [LogSettings::get_verbosity_level].
pub tracing_level: String,
/// Enable console log output.
pub pretty_log: Option<bool>,
/// Is the application running by systemd? If so, log to journald.
pub journald: Option<bool>,
}
/// Settings for the email client [mailjet_client](https://crates.io/crates/mailjet_client)
#[derive(Clone, Debug, Deserialize)]
pub struct EmailClientSettings {
pub api_user: SecretString,
pub api_key: SecretString,
pub user_agent: String,
pub target_api: String,
pub admin_address: SecretString,
pub sandbox_mode: Option<bool>,
}
impl Settings {
/// Parse the application settings.
pub fn new() -> Result<Self, ConfigError> {
// Build the full path of the configuration directory.
let base_path =
std::env::current_dir().expect("Failed to determine the current directory.");
let cfg_dir = base_path.join(CONF_DIR);
let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "devel".into());
let settings = Config::builder()
// Start of by merging in the "default" configuration file.
.add_source(File::from(cfg_dir.join("base")).required(true))
.add_source(File::from(cfg_dir.join(run_mode)).required(false))
.add_source(File::from(cfg_dir.join("local")).required(false))
.add_source(Environment::with_prefix("lacoctelera").separator("__"))
.build()?;
settings.try_deserialize()
}
}
impl DataBaseSettings {
pub fn connection_string(&self) -> SecretString {
SecretString::from(format!(
"mysql://{}:{}@{}:{}/{}",
self.username,
self.password.expose_secret(),
self.host,
self.port,
self.db_name,
))
}
/// Translate a timeout in seconds from an integer to a type `time::Duration`.
pub fn idle_timeout(&self) -> time::Duration {
Duration::from_secs(self.idle_timeout_sec as u64)
}
/// Build a connection to the MariaDB server without using a DB name.
///
/// # Description
///
/// The following settings will be applied:
/// - [DataBaseSettings::host]
/// - [DataBaseSettings::username]
/// - [DataBaseSettings::password]
/// - [DataBaseSettings::port]
/// - [DataBaseSettings::require_ssl]
pub fn build_db_conn_without_db(&self) -> MySqlConnectOptions {
MySqlConnectOptions::new()
.host(&self.host)
.username(&self.username)
.password(self.password.expose_secret())
.port(self.port)
.charset("utf8mb4")
.ssl_mode(if self.require_ssl {
MySqlSslMode::Required
} else {
MySqlSslMode::Preferred
})
}
/// Build a connection to the MariaDB server without using a DB name.
///
/// # Description
///
/// The following settings will be applied plus the ones from [DataBaseSettings::build_db_conn_without_db]:
/// - [DataBaseSettings::db_name]
pub fn build_db_conn_with_db(&self) -> MySqlConnectOptions {
self.build_db_conn_without_db().database(&self.db_name)
}
}
impl LogSettings {
/// Get the chosen verbosity level as a [LevelFilter] object.
///
/// # Description
///
/// Translate the tracing level that is given via a configuration file into a
/// [LevelFilter] object. Such object can be passed straight to a `Subscriber` to
/// specify a filter for the log messages.
///
/// Accepted values:
/// - `debug` or `dbg` to set the verbiosity to `DEBUG`.
/// - `info` to set the verbosity to `INFO`.
/// - `error` or `err` to set the verbosity to `ERROR`.
/// - `trace` to set the verbosity to `TRACE`.
/// - `warn` or any other string to set the verbosity to `WARN`.
pub fn get_verbosity_level(&self) -> LevelFilter {
LogSettings::verbosity(&self.tracing_level)
}
/// Translate a string into a [LevelFilter] or return a [LevelFilter::WARN] by default.
fn verbosity(level: &str) -> LevelFilter {
match level {
"debug" | "dbg" => LevelFilter::DEBUG,
"info" => LevelFilter::INFO,
"error" | "err" => LevelFilter::ERROR,
"trace" => LevelFilter::TRACE,
_ => LevelFilter::WARN,
}
}
}