From 02ec18a88fada40f4c48cdde9127e98694d56e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Hru=C5=A1ka?= Date: Sun, 19 Apr 2020 19:24:36 +0200 Subject: [PATCH] Initial --- .gitignore | 2 + Cargo.toml | 16 ++++ src/lib.rs | 210 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 228 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9e016d7 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "cli-app-base" +version = "0.1.0" +authors = ["Ondřej Hruška "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4" +env_logger = "0.7.1" +failure = "0.1" +serde = "1.0.106" +serde_json = "1.0.51" +clap = "2.33.0" +json5 = "0.2.7" diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..cff0de9 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,210 @@ +#[macro_use] +extern crate log; + +use failure::{bail, Fallible}; +use std::collections::HashMap; +use std::str::FromStr; +use serde::{Serialize}; +use std::fmt::Debug; +use std::path::{Path, PathBuf}; +use std::fs::File; +use std::io::Read; +use serde::de::DeserializeOwned; + +pub const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"]; + +/// 3rd-party libraries that produce log spam - we set these to a fixed higher level +/// to allow using e.g. TRACE without drowning our custom messages +pub const SPAMMY_LIBS: [&str; 6] = [ + "tokio_reactor", + "hyper", + "reqwest", + "mio", + "want", + "rumqtt", +]; + +/// Implement this for the main config struct +pub trait BoilerplateCfg { + type Init; + + /// Get log level + fn logging(&self) -> &String; + + /// Get names of library modules to suppress from log output (limit to warn or error) + fn logging_suppress_mods(&self) -> Option> { + Some(Vec::from(&SPAMMY_LIBS[..])) + } + + /// Get log module levels to use (take priority over the main log level) + fn logging_mod_levels(&self) -> Option> { + None + } + + /// Add args to later use in the `configure` method. + fn add_args<'a: 'b, 'b>(clap: clap::App<'a, 'b>) -> clap::App<'a, 'b> { + // Default impl + clap + } + + /// Configure the config object using args. + /// Logging has already been inited and configured. + fn configure<'a>(self, _clap: &clap::ArgMatches<'a>) -> Fallible; +} + +pub trait Boilerplate : BoilerplateCfg + Sized { + fn init(name: &str, cfg_file_name: &str, version : Option) -> Fallible; +} + +fn read_file>(path: P) -> Fallible { + let path = path.as_ref(); + let mut file = File::open(path)?; + + let mut buf = String::new(); + file.read_to_string(&mut buf)?; + Ok(buf) +} + +impl Boilerplate for CFG { + /// Initialize the app + fn init(name: &str, cfg_file_name: &str, version : Option) -> Fallible + { + let version : String = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").into()); + let clap = + clap::App::new(name) + .arg(clap::Arg::with_name("config") + .short("c").long("config") + .value_name("FILE") + .help("Sets a custom config file (JSON5)") + .takes_value(true)) + + .arg(clap::Arg::with_name("dump-config") + .long("dump-config") + .takes_value(false) + .help("Print the loaded config struct")) + + .arg(clap::Arg::with_name("default-config") + .long("default-config") + .takes_value(false) + .help("Print the default config JSON for reference (or to be piped to a file)")) + + .arg(clap::Arg::with_name("verbose") + .short("v").long("verbose").multiple(true) + .help("Increase logging verbosity (repeat to increase)")) + + .arg(clap::Arg::with_name("log-level") + .long("log") + .takes_value(true).value_name("LEVEL") + .validator(|s| { + if LOG_LEVELS.contains(&s.as_str()) { return Ok(()); } + Err(format!("Bad log level: {}", s)) + }) + .help("Set logging verbosity (error,warning,info,debug,trace)")); + + let clap = Self::add_args(clap); + + // this must be done after `add_args` or all hell breaks loose around lifetimes + let clap = clap + .version(version.as_str()); + + let argv = clap.get_matches(); + + if argv.is_present("default-config") { + println!("{}", serde_json::to_string_pretty(&CFG::default()).unwrap()); + std::process::exit(0); + } + + /* Load config */ + let cfg_file = argv.value_of("config").unwrap_or(cfg_file_name); + + println!("{} {}\nRun with -h for help", name, version); + + let filepath = PathBuf::from_str(cfg_file)?; + + let config: CFG = if filepath.is_file() { + println!("Load config from \"{}\"", cfg_file); + + let buf = read_file(filepath)?; + json5::from_str(&buf)? + } else { + println!("Config file \"{}\" does not exist, using defaults.", cfg_file); + + CFG::default() + }; + + let mut level = config.logging().as_str(); + + if !LOG_LEVELS.contains(&level) { + bail!("Invalid default log level: {}", level); + } + + /* env RUST_LOG overrides default if set, but can be changed by CLI args */ + let env_level = option_env!("RUST_LOG").unwrap_or(""); + if !env_level.is_empty() { + level = env_level; + + if !LOG_LEVELS.contains(&level) { + bail!("Invalid env log level: {}", level); + } + } + + /* Explicitly requested level */ + if let Some(l) = argv.value_of("log-level") { + level = l; // validated by clap + } + + /* Verbosity increased */ + if argv.is_present("verbose") { + // bump verbosity if -v's are present + let pos = LOG_LEVELS + .iter() + .position(|x| x == &level) + .unwrap(); + + level = match LOG_LEVELS + .iter() + .nth(pos + argv.occurrences_of("verbose") as usize) + { + Some(new_level) => new_level, + None => "trace", + }; + } + + let env = env_logger::Env::default().default_filter_or(level); + let mut builder = env_logger::Builder::from_env(env); + + // add a newline + println!(); + + builder.format_timestamp_millis(); + + if let Some(suppress_mods) = config.logging_suppress_mods() { + // set logging level for suppressed libs + let mut sup_lvl = log::LevelFilter::Info; + if level == "warn" { + sup_lvl = log::LevelFilter::Warn; + } else if level == "error" { + sup_lvl = log::LevelFilter::Error; + } + + for lib in suppress_mods { + builder.filter_module(lib, sup_lvl); + } + } + + if let Some(mod_levels) = config.logging_mod_levels() { + for (module, lvl) in mod_levels { + let lvl = log::LevelFilter::from_str(lvl.as_ref())?; + builder.filter_module(module.as_ref(), lvl); + } + } + + builder.init(); + + if argv.is_present("dump-config") { + debug!("Loaded config: {:#?}", config); + } + + config.configure(&argv) + } +}