commit
02ec18a88f
@ -0,0 +1,2 @@ |
|||||||
|
/target |
||||||
|
Cargo.lock |
@ -0,0 +1,16 @@ |
|||||||
|
[package] |
||||||
|
name = "cli-app-base" |
||||||
|
version = "0.1.0" |
||||||
|
authors = ["Ondřej Hruška <ondra@ondrovo.com>"] |
||||||
|
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" |
@ -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<Vec<&str>> { |
||||||
|
Some(Vec::from(&SPAMMY_LIBS[..])) |
||||||
|
} |
||||||
|
|
||||||
|
/// Get log module levels to use (take priority over the main log level)
|
||||||
|
fn logging_mod_levels(&self) -> Option<HashMap<String, String>> { |
||||||
|
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<Self::Init>; |
||||||
|
} |
||||||
|
|
||||||
|
pub trait Boilerplate : BoilerplateCfg + Sized { |
||||||
|
fn init(name: &str, cfg_file_name: &str, version : Option<String>) -> Fallible<Self::Init>; |
||||||
|
} |
||||||
|
|
||||||
|
fn read_file<P: AsRef<Path>>(path: P) -> Fallible<String> { |
||||||
|
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<CFG : BoilerplateCfg + Serialize + DeserializeOwned + Debug + Default> Boilerplate for CFG { |
||||||
|
/// Initialize the app
|
||||||
|
fn init(name: &str, cfg_file_name: &str, version : Option<String>) -> Fallible<CFG::Init> |
||||||
|
{ |
||||||
|
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) |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue