diff --git a/lib/core/libimagrt/src/error.rs b/lib/core/libimagrt/src/error.rs index 388335af..ff992291 100644 --- a/lib/core/libimagrt/src/error.rs +++ b/lib/core/libimagrt/src/error.rs @@ -23,7 +23,13 @@ use std::io::Error as IOError; generate_error_types!(RuntimeError, RuntimeErrorKind, Instantiate => "Could not instantiate", IOError => "IO Error", - ProcessExitFailure => "Process exited with failure" + IOLogFileOpenError => "IO Error: Could not open logfile", + ProcessExitFailure => "Process exited with failure", + ConfigReadError => "Error while reading the configuration", + ConfigTypeError => "Error while reading the configuration: Type Error", + GlobalLogLevelConfigMissing => "Global config 'imag.logging.level' missing", + InvalidLogLevelSpec => "Invalid log level specification: Only 'trace', 'debug', 'info', 'warn', 'error' are allowed", + TomlReadError => "Error while reading in TOML document" ); impl From for RuntimeError { diff --git a/lib/core/libimagrt/src/lib.rs b/lib/core/libimagrt/src/lib.rs index ce0e08c1..addce7b0 100644 --- a/lib/core/libimagrt/src/lib.rs +++ b/lib/core/libimagrt/src/lib.rs @@ -38,6 +38,7 @@ extern crate itertools; #[cfg(unix)] extern crate xdg_basedir; extern crate env_logger; extern crate ansi_term; +extern crate handlebars; extern crate clap; extern crate toml; diff --git a/lib/core/libimagrt/src/logger.rs b/lib/core/libimagrt/src/logger.rs index 992a9d94..e80a4c7b 100644 --- a/lib/core/libimagrt/src/logger.rs +++ b/lib/core/libimagrt/src/logger.rs @@ -19,67 +19,75 @@ use std::io::Write; use std::io::stderr; +use std::collections::BTreeMap; +use configuration::Configuration; +use error::RuntimeErrorKind as EK; +use error::RuntimeError; +use error::MapErrInto; +use runtime::Runtime; + +use libimagerror::into::IntoError; + +use clap::ArgMatches; use log::{Log, LogLevel, LogRecord, LogMetadata}; +use toml::Value; +use toml_query::read::TomlValueReadExt; -use ansi_term::Style; -use ansi_term::Colour; -use ansi_term::ANSIString; +type ModuleName = String; +type Result = ::std::result::Result; + +enum LogDestination { + Stderr, + File(::std::fs::File), +} + +impl Default for LogDestination { + fn default() -> LogDestination { + LogDestination::Stderr + } +} + +struct ModuleSettings { + enabled: bool, + level: LogLevel, + + #[allow(unused)] + destinations: Vec, +} /// Logger implementation for `log` crate. pub struct ImagLogger { - prefix: String, - dbg_fileline: bool, - lvl: LogLevel, - color_enabled: bool, + global_loglevel : LogLevel, + + #[allow(unused)] + global_destinations : Vec, + // global_format_trace : , + // global_format_debug : , + // global_format_info : , + // global_format_warn : , + // global_format_error : , + module_settings : BTreeMap, } impl ImagLogger { /// Create a new ImagLogger object with a certain level - pub fn new(lvl: LogLevel) -> ImagLogger { - ImagLogger { - prefix: "[imag]".to_owned(), - dbg_fileline: true, - lvl: lvl, - color_enabled: true - } + pub fn new(matches: &ArgMatches, config: Option<&Configuration>) -> Result { + Ok(ImagLogger { + global_loglevel : try!(aggregate_global_loglevel(matches, config)), + global_destinations : try!(aggregate_global_destinations(matches, config)), + // global_format_trace : try!(aggregate_global_format_trace(matches, config)), + // global_format_debug : try!(aggregate_global_format_debug(matches, config)), + // global_format_info : try!(aggregate_global_format_info(matches, config)), + // global_format_warn : try!(aggregate_global_format_warn(matches, config)), + // global_format_error : try!(aggregate_global_format_error(matches, config)), + module_settings : try!(aggregate_module_settings(matches, config)), + }) } - /// Set debugging to include file and line - pub fn with_dbg_file_and_line(mut self, b: bool) -> ImagLogger { - self.dbg_fileline = b; - self - } - - /// Set debugging to include prefix - pub fn with_prefix(mut self, pref: String) -> ImagLogger { - self.prefix = pref; - self - } - - /// Set debugging to have color - pub fn with_color(mut self, b: bool) -> ImagLogger { - self.color_enabled = b; - self - } - - /// Helper function to colorize a string with a certain Style - fn style_or_not(&self, c: Style, s: String) -> ANSIString { - if self.color_enabled { - c.paint(s) - } else { - ANSIString::from(s) - } - } - - /// Helper function to colorize a string with a certain Color - fn color_or_not(&self, c: Colour, s: String) -> ANSIString { - if self.color_enabled { - c.paint(s) - } else { - ANSIString::from(s) - } + pub fn global_loglevel(&self) -> LogLevel { + self.global_loglevel } } @@ -87,47 +95,217 @@ impl ImagLogger { impl Log for ImagLogger { fn enabled(&self, metadata: &LogMetadata) -> bool { - metadata.level() <= self.lvl + metadata.level() <= self.global_loglevel } fn log(&self, record: &LogRecord) { - use ansi_term::Colour::Red; - use ansi_term::Colour::Yellow; - use ansi_term::Colour::Cyan; + let log_level = record.level(); + let log_location = record.location(); + let log_target = record.target(); - if self.enabled(record.metadata()) { - // TODO: This is just simple logging. Maybe we can enhance this lateron - let loc = record.location(); - match record.metadata().level() { - LogLevel::Debug => { - let lvl = self.color_or_not(Cyan, format!("{}", record.level())); - let args = self.color_or_not(Cyan, format!("{}", record.args())); - if self.dbg_fileline { - let file = self.color_or_not(Cyan, format!("{}", loc.file())); - let ln = self.color_or_not(Cyan, format!("{}", loc.line())); + self.module_settings + .get(log_target) + .map(|module_setting| { + if module_setting.enabled && module_setting.level >= log_level { + write!(stderr(), "[imag][{}]: {}", log_level, record.args()).ok(); + } + }) + .unwrap_or_else(|| { + if self.global_loglevel >= log_level { + // Yes, we log + write!(stderr(), "[imag][{}]: {}", log_level, record.args()).ok(); + } + }); + } +} - writeln!(stderr(), "{}[{: <5}][{}][{: >5}]: {}", self.prefix, lvl, file, ln, args).ok(); - } else { - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok(); - } - }, - LogLevel::Warn | LogLevel::Error => { - let lvl = self.style_or_not(Red.blink(), format!("{}", record.level())); - let args = self.color_or_not(Red, format!("{}", record.args())); +fn match_log_level_str(s: &str) -> Result { + match s { + "trace" => Ok(LogLevel::Trace), + "debug" => Ok(LogLevel::Debug), + "info" => Ok(LogLevel::Info), + "warn" => Ok(LogLevel::Warn), + "error" => Ok(LogLevel::Error), + _ => return Err(EK::InvalidLogLevelSpec.into_error()), + } +} - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok(); - }, - LogLevel::Info => { - let lvl = self.color_or_not(Yellow, format!("{}", record.level())); - let args = self.color_or_not(Yellow, format!("{}", record.args())); +fn aggregate_global_loglevel(matches: &ArgMatches, config: Option<&Configuration>) + -> Result +{ + match config { + Some(cfg) => match cfg + .read("imag.logging.level") + .map_err_into(EK::ConfigReadError) + { + Ok(Some(&Value::String(ref s))) => match_log_level_str(s), + Ok(Some(_)) => Err(EK::ConfigTypeError.into_error()), + Ok(None) => Err(EK::GlobalLogLevelConfigMissing.into_error()), + Err(e) => Err(e) + }, + None => { + if matches.is_present(Runtime::arg_debugging_name()) { + return Ok(LogLevel::Debug) + } - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok(); - }, - _ => { - writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, record.level(), record.args()).ok(); - }, + matches + .value_of(Runtime::arg_verbosity_name()) + .map(match_log_level_str) + .unwrap_or(Ok(LogLevel::Info)) + } + } +} + +fn translate_destination(raw: &str) -> Result { + use std::fs::OpenOptions; + + match raw { + "-" => Ok(LogDestination::Stderr), + other => { + OpenOptions::new() + .append(true) + .create(true) + .open(other) + .map(LogDestination::File) + .map_err_into(EK::IOLogFileOpenError) + } + } +} + + +fn translate_destinations(raw: &Vec) -> Result> { + raw.iter() + .fold(Ok(vec![]), |acc, val| { + acc.and_then(|mut v| { + let dest = match *val { + Value::String(ref s) => try!(translate_destination(s)), + _ => return Err(EK::ConfigTypeError.into_error()), + }; + v.push(dest); + Ok(v) + }) + }) +} + +fn aggregate_global_destinations(matches: &ArgMatches, config: Option<&Configuration>) + -> Result> +{ + + match config { + Some(cfg) => match cfg + .read("imag.logging.destinations") + .map_err_into(EK::ConfigReadError) + { + Ok(Some(&Value::Array(ref a))) => translate_destinations(a), + Ok(Some(_)) => Err(EK::ConfigTypeError.into_error()), + Ok(None) => Err(EK::GlobalLogLevelConfigMissing.into_error()), + Err(e) => Err(e) + }, + None => { + if let Some(values) = matches.value_of(Runtime::arg_logdest_name()) { + // parse logdest specification from commandline + + values.split(",") + .fold(Ok(vec![]), move |acc, dest| { + acc.and_then(|mut v| { + v.push(try!(translate_destination(dest))); + Ok(v) + }) + }) + } else { + Ok(vec![ LogDestination::default() ]) } } } } +// fn aggregate_global_format_trace(matches: &ArgMatches, config: Option<&Configuration>) +// -> +// { +// unimplemented!() +// } +// +// fn aggregate_global_format_debug(matches: &ArgMatches, config: Option<&Configuration>) +// -> +// { +// unimplemented!() +// } +// +// fn aggregate_global_format_info(matches: &ArgMatches, config: Option<&Configuration>) +// -> +// { +// unimplemented!() +// } +// +// fn aggregate_global_format_warn(matches: &ArgMatches, config: Option<&Configuration>) +// -> +// { +// unimplemented!() +// } +// +// fn aggregate_global_format_error(matches: &ArgMatches, config: Option<&Configuration>) +// -> +// { +// unimplemented!() +// } + +fn aggregate_module_settings(matches: &ArgMatches, config: Option<&Configuration>) + -> Result> +{ + match config { + Some(cfg) => match cfg + .read("imag.logging.modules") + .map_err_into(EK::ConfigReadError) + { + Ok(Some(&Value::Table(ref t))) => { + // translate the module settings from the table `t` + let mut settings = BTreeMap::new(); + + for (module_name, v) in t { + let destinations = try!(match v.read("destinations") { + Ok(Some(&Value::Array(ref a))) => translate_destinations(a), + Ok(Some(_)) => Err(EK::ConfigTypeError.into_error()), + Ok(None) => Err(EK::GlobalLogLevelConfigMissing.into_error()), + Err(e) => Err(e).map_err_into(EK::TomlReadError), + }); + + let level = try!(match v.read("level") { + Ok(Some(&Value::String(ref s))) => match_log_level_str(s), + Ok(Some(_)) => Err(EK::ConfigTypeError.into_error()), + Ok(None) => Err(EK::GlobalLogLevelConfigMissing.into_error()), + Err(e) => Err(e).map_err_into(EK::TomlReadError), + }); + + let enabled = try!(match v.read("enabled") { + Ok(Some(&Value::Boolean(b))) => Ok(b), + Ok(Some(_)) => Err(EK::ConfigTypeError.into_error()), + Ok(None) => Err(EK::GlobalLogLevelConfigMissing.into_error()), + Err(e) => Err(e).map_err_into(EK::TomlReadError), + }); + + let module_settings = ModuleSettings { + enabled: enabled, + level: level, + destinations: destinations, + }; + + // We don't care whether there was a value, we override it. + let _ = settings.insert(module_name.to_owned(), module_settings); + } + + Ok(settings) + }, + Ok(Some(_)) => Err(EK::ConfigTypeError.into_error()), + Ok(None) => Err(EK::GlobalLogLevelConfigMissing.into_error()), + Err(e) => Err(e), + }, + None => { + write!(stderr(), "No Configuration.").ok(); + write!(stderr(), "cannot find module-settings for logging.").ok(); + write!(stderr(), "Will use global defaults").ok(); + + Ok(BTreeMap::new()) + } + } +} + diff --git a/lib/core/libimagrt/src/runtime.rs b/lib/core/libimagrt/src/runtime.rs index d30ca3aa..d4f29f1c 100644 --- a/lib/core/libimagrt/src/runtime.rs +++ b/lib/core/libimagrt/src/runtime.rs @@ -20,14 +20,12 @@ use std::path::PathBuf; use std::process::Command; use std::env; -use std::io::stderr; -use std::io::Write; +use std::process::exit; pub use clap::App; use clap::{Arg, ArgMatches}; use log; -use log::LogLevelFilter; use configuration::{Configuration, InternalConfiguration}; use error::RuntimeError; @@ -35,6 +33,7 @@ use error::RuntimeErrorKind; use error::MapErrInto; use logger::ImagLogger; +use libimagerror::trace::*; use libimagstore::store::Store; use libimagstore::file_abstraction::InMemoryFileAbstraction; use spec::CliSpec; @@ -112,16 +111,10 @@ impl<'a> Runtime<'a> { where C: Clone + CliSpec<'a> + InternalConfiguration { use std::io::stdout; - use clap::Shell; - let is_debugging = matches.is_present(Runtime::arg_debugging_name()); - if cli_app.enable_logging() { - let is_verbose = matches.is_present(Runtime::arg_verbosity_name()); - let colored = !matches.is_present(Runtime::arg_no_color_output_name()); - - Runtime::init_logger(is_debugging, is_verbose, colored); + Runtime::init_logger(&matches, config.as_ref()) } match matches.value_of(Runtime::arg_generate_compl()) { @@ -151,9 +144,9 @@ impl<'a> Runtime<'a> { None => None, }; - if is_debugging { - write!(stderr(), "Config: {:?}\n", config).ok(); - write!(stderr(), "Store-config: {:?}\n", store_config).ok(); + if matches.is_present(Runtime::arg_debugging_name()) { + debug!("Config: {:?}\n", config); + debug!("Store-config: {:?}\n", store_config); } let store_result = if cli_app.use_inmemory_fs() { @@ -200,9 +193,11 @@ impl<'a> Runtime<'a> { .arg(Arg::with_name(Runtime::arg_verbosity_name()) .short("v") .long("verbose") - .help("Enables verbosity") + .help("Enables verbosity, can be used to set log level to one of 'trace', 'debug', 'info', 'warn' or 'error'") .required(false) - .takes_value(false)) + .takes_value(true) + .possible_values(&["trace", "debug", "info", "warn", "error"]) + .value_name("LOGLEVEL")) .arg(Arg::with_name(Runtime::arg_debugging_name()) .long("debug") @@ -254,6 +249,21 @@ impl<'a> Runtime<'a> { .value_name("SHELL") .possible_values(&["bash", "fish", "zsh"])) + .arg(Arg::with_name(Runtime::arg_logdest_name()) + .long(Runtime::arg_logdest_name()) + .help("Override the logging destinations from the configuration: values can be seperated by ',', a value of '-' marks the stderr output, everything else is expected to be a path") + .required(false) + .takes_value(true) + .value_name("LOGDESTS")) + + .arg(Arg::with_name(Runtime::arg_override_module_logging_setting_name()) + .long(Runtime::arg_override_module_logging_setting_name()) + .help("Override a module logging setting for one module. Format: ==, whereas is either 'enabled', 'level' or 'destinations' - This commandline argument is CURRENTLY NOT IMPLEMENTED") + .multiple(true) + .required(false) + .takes_value(true) + .value_name("SPEC")) + } /// Get the argument names of the Runtime which are available @@ -338,26 +348,30 @@ impl<'a> Runtime<'a> { self } + /// Get the argument name for the logging destination + pub fn arg_logdest_name() -> &'static str { + "logging-destinations" + } + + pub fn arg_override_module_logging_setting_name() -> &'static str { + "override-module-log-setting" + } + /// Initialize the internal logger - fn init_logger(is_debugging: bool, is_verbose: bool, colored: bool) { + fn init_logger(matches: &ArgMatches, config: Option<&Configuration>) { use std::env::var as env_var; use env_logger; if env_var("IMAG_LOG_ENV").is_ok() { env_logger::init().unwrap(); } else { - let lvl = if is_debugging { - LogLevelFilter::Debug - } else if is_verbose { - LogLevelFilter::Info - } else { - LogLevelFilter::Warn - }; - log::set_logger(|max_log_lvl| { - max_log_lvl.set(lvl); - debug!("Init logger with {}", lvl); - Box::new(ImagLogger::new(lvl.to_log_level().unwrap()).with_color(colored)) + let logger = ImagLogger::new(matches, config) + .map_err_trace() + .unwrap_or_else(|_| exit(1)); + max_log_lvl.set(logger.global_loglevel().to_log_level_filter()); + debug!("Init logger with {}", logger.global_loglevel()); + Box::new(logger) }) .map_err(|e| panic!("Could not setup logger: {:?}", e)) .ok();