Merge pull request #1023 from matthiasbeyer/rewrite-logging
Rewrite logging
This commit is contained in:
commit
25ffb60d7b
7 changed files with 661 additions and 108 deletions
|
@ -25,6 +25,7 @@ This section contains the changelog from the last release to the next release.
|
||||||
* The codebase was moved to a more tree-ish approach, where several
|
* The codebase was moved to a more tree-ish approach, where several
|
||||||
subdirectories were introduced for different types of crates
|
subdirectories were introduced for different types of crates
|
||||||
* The documentation got a major overhaul and was partly rewritten
|
* The documentation got a major overhaul and was partly rewritten
|
||||||
|
* The logger is now configurable via the config file.
|
||||||
* New
|
* New
|
||||||
* `libimagentrygps` was introduced
|
* `libimagentrygps` was introduced
|
||||||
* Fixed bugs
|
* Fixed bugs
|
||||||
|
|
29
imagrc.toml
29
imagrc.toml
|
@ -1,6 +1,35 @@
|
||||||
# This is a example configuration file for the imag suite.
|
# This is a example configuration file for the imag suite.
|
||||||
# It is written in TOML
|
# It is written in TOML
|
||||||
|
|
||||||
|
[imag.logging]
|
||||||
|
level = "debug"
|
||||||
|
destinations = [ "-" ]
|
||||||
|
|
||||||
|
# Valid variables for logging:
|
||||||
|
# * "level"
|
||||||
|
# * "module_path"
|
||||||
|
# * "file"
|
||||||
|
# * "line"
|
||||||
|
# * "target"
|
||||||
|
# * "message"
|
||||||
|
#
|
||||||
|
# Valid functions to be applied:
|
||||||
|
# * "black"
|
||||||
|
# * "blue"
|
||||||
|
# * "cyan"
|
||||||
|
# * "green"
|
||||||
|
# * "purple"
|
||||||
|
# * "red"
|
||||||
|
# * "white"
|
||||||
|
# * "yellow"
|
||||||
|
|
||||||
|
[imag.logging.format]
|
||||||
|
trace = "[imag][{{red level}}][{{module_path}}]: {{message}}"
|
||||||
|
debug = "[imag][{{cyan level}}]: {{message}}"
|
||||||
|
info = "[imag]: {{message}}"
|
||||||
|
warn = "[imag][{{bold level}}]: {{yellow message}}"
|
||||||
|
error = "[imag][{{red level}}]: {{red message}}"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Configuration options for the user interface
|
# Configuration options for the user interface
|
||||||
#
|
#
|
||||||
|
|
|
@ -23,6 +23,7 @@ itertools = "0.5"
|
||||||
ansi_term = "0.9"
|
ansi_term = "0.9"
|
||||||
is-match = "0.1"
|
is-match = "0.1"
|
||||||
toml-query = "0.3.0"
|
toml-query = "0.3.0"
|
||||||
|
handlebars = "0.29.0"
|
||||||
|
|
||||||
libimagstore = { version = "0.4.0", path = "../../../lib/core/libimagstore" }
|
libimagstore = { version = "0.4.0", path = "../../../lib/core/libimagstore" }
|
||||||
libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" }
|
libimagerror = { version = "0.4.0", path = "../../../lib/core/libimagerror" }
|
||||||
|
|
|
@ -23,7 +23,20 @@ use std::io::Error as IOError;
|
||||||
generate_error_types!(RuntimeError, RuntimeErrorKind,
|
generate_error_types!(RuntimeError, RuntimeErrorKind,
|
||||||
Instantiate => "Could not instantiate",
|
Instantiate => "Could not instantiate",
|
||||||
IOError => "IO Error",
|
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",
|
||||||
|
GlobalDestinationConfigMissing => "Global config 'imag.logging.destinations' missing",
|
||||||
|
InvalidLogLevelSpec => "Invalid log level specification: Only 'trace', 'debug', 'info', 'warn', 'error' are allowed",
|
||||||
|
TomlReadError => "Error while reading in TOML document",
|
||||||
|
TemplateStringRegistrationError => "Error while registering logging template string",
|
||||||
|
ConfigMissingLoggingFormatTrace => "Missing config for logging format for trace logging",
|
||||||
|
ConfigMissingLoggingFormatDebug => "Missing config for logging format for debug logging",
|
||||||
|
ConfigMissingLoggingFormatInfo => "Missing config for logging format for info logging",
|
||||||
|
ConfigMissingLoggingFormatWarn => "Missing config for logging format for warn logging",
|
||||||
|
ConfigMissingLoggingFormatError => "Missing config for logging format for error logging"
|
||||||
);
|
);
|
||||||
|
|
||||||
impl From<IOError> for RuntimeError {
|
impl From<IOError> for RuntimeError {
|
||||||
|
|
|
@ -38,6 +38,7 @@ extern crate itertools;
|
||||||
#[cfg(unix)] extern crate xdg_basedir;
|
#[cfg(unix)] extern crate xdg_basedir;
|
||||||
extern crate env_logger;
|
extern crate env_logger;
|
||||||
extern crate ansi_term;
|
extern crate ansi_term;
|
||||||
|
extern crate handlebars;
|
||||||
|
|
||||||
extern crate clap;
|
extern crate clap;
|
||||||
extern crate toml;
|
extern crate toml;
|
||||||
|
|
|
@ -19,67 +19,116 @@
|
||||||
|
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::io::stderr;
|
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 log::{Log, LogLevel, LogRecord, LogMetadata};
|
||||||
|
use toml::Value;
|
||||||
|
use toml_query::read::TomlValueReadExt;
|
||||||
|
use handlebars::Handlebars;
|
||||||
|
|
||||||
use ansi_term::Style;
|
type ModuleName = String;
|
||||||
use ansi_term::Colour;
|
type Result<T> = ::std::result::Result<T, RuntimeError>;
|
||||||
use ansi_term::ANSIString;
|
|
||||||
|
enum LogDestination {
|
||||||
|
Stderr,
|
||||||
|
File(::std::fs::File),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogDestination {
|
||||||
|
fn default() -> LogDestination {
|
||||||
|
LogDestination::Stderr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ModuleSettings {
|
||||||
|
enabled: bool,
|
||||||
|
level: Option<LogLevel>,
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
destinations: Option<Vec<LogDestination>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Logger implementation for `log` crate.
|
/// Logger implementation for `log` crate.
|
||||||
pub struct ImagLogger {
|
pub struct ImagLogger {
|
||||||
prefix: String,
|
global_loglevel : LogLevel,
|
||||||
dbg_fileline: bool,
|
|
||||||
lvl: LogLevel,
|
#[allow(unused)]
|
||||||
color_enabled: bool,
|
global_destinations : Vec<LogDestination>,
|
||||||
|
// global_format_trace : ,
|
||||||
|
// global_format_debug : ,
|
||||||
|
// global_format_info : ,
|
||||||
|
// global_format_warn : ,
|
||||||
|
// global_format_error : ,
|
||||||
|
module_settings : BTreeMap<ModuleName, ModuleSettings>,
|
||||||
|
|
||||||
|
handlebars: Handlebars,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImagLogger {
|
impl ImagLogger {
|
||||||
|
|
||||||
/// Create a new ImagLogger object with a certain level
|
/// Create a new ImagLogger object with a certain level
|
||||||
pub fn new(lvl: LogLevel) -> ImagLogger {
|
pub fn new(matches: &ArgMatches, config: Option<&Configuration>) -> Result<ImagLogger> {
|
||||||
ImagLogger {
|
let mut handlebars = Handlebars::new();
|
||||||
prefix: "[imag]".to_owned(),
|
|
||||||
dbg_fileline: true,
|
handlebars.register_helper("black" , Box::new(self::template_helpers::ColorizeBlackHelper));
|
||||||
lvl: lvl,
|
handlebars.register_helper("blue" , Box::new(self::template_helpers::ColorizeBlueHelper));
|
||||||
color_enabled: true
|
handlebars.register_helper("cyan" , Box::new(self::template_helpers::ColorizeCyanHelper));
|
||||||
|
handlebars.register_helper("green" , Box::new(self::template_helpers::ColorizeGreenHelper));
|
||||||
|
handlebars.register_helper("purple" , Box::new(self::template_helpers::ColorizePurpleHelper));
|
||||||
|
handlebars.register_helper("red" , Box::new(self::template_helpers::ColorizeRedHelper));
|
||||||
|
handlebars.register_helper("white" , Box::new(self::template_helpers::ColorizeWhiteHelper));
|
||||||
|
handlebars.register_helper("yellow" , Box::new(self::template_helpers::ColorizeYellowHelper));
|
||||||
|
|
||||||
|
handlebars.register_helper("underline" , Box::new(self::template_helpers::UnderlineHelper));
|
||||||
|
handlebars.register_helper("bold" , Box::new(self::template_helpers::BoldHelper));
|
||||||
|
handlebars.register_helper("blink" , Box::new(self::template_helpers::BlinkHelper));
|
||||||
|
handlebars.register_helper("strikethrough" , Box::new(self::template_helpers::StrikethroughHelper));
|
||||||
|
|
||||||
|
{
|
||||||
|
let fmt = try!(aggregate_global_format_trace(matches, config));
|
||||||
|
try!(handlebars.register_template_string("TRACE", fmt) // name must be uppercase
|
||||||
|
.map_err_into(EK::TemplateStringRegistrationError));
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
let fmt = try!(aggregate_global_format_debug(matches, config));
|
||||||
|
try!(handlebars.register_template_string("DEBUG", fmt) // name must be uppercase
|
||||||
|
.map_err_into(EK::TemplateStringRegistrationError));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let fmt = try!(aggregate_global_format_info(matches, config));
|
||||||
|
try!(handlebars.register_template_string("INFO", fmt) // name must be uppercase
|
||||||
|
.map_err_into(EK::TemplateStringRegistrationError));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let fmt = try!(aggregate_global_format_warn(matches, config));
|
||||||
|
try!(handlebars.register_template_string("WARN", fmt) // name must be uppercase
|
||||||
|
.map_err_into(EK::TemplateStringRegistrationError));
|
||||||
|
}
|
||||||
|
{
|
||||||
|
let fmt = try!(aggregate_global_format_error(matches, config));
|
||||||
|
try!(handlebars.register_template_string("ERROR", fmt) // name must be uppercase
|
||||||
|
.map_err_into(EK::TemplateStringRegistrationError));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set debugging to include file and line
|
Ok(ImagLogger {
|
||||||
pub fn with_dbg_file_and_line(mut self, b: bool) -> ImagLogger {
|
global_loglevel : try!(aggregate_global_loglevel(matches, config)),
|
||||||
self.dbg_fileline = b;
|
global_destinations : try!(aggregate_global_destinations(matches, config)),
|
||||||
self
|
module_settings : try!(aggregate_module_settings(matches, config)),
|
||||||
|
handlebars : handlebars,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set debugging to include prefix
|
pub fn global_loglevel(&self) -> LogLevel {
|
||||||
pub fn with_prefix(mut self, pref: String) -> ImagLogger {
|
self.global_loglevel
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -87,47 +136,432 @@ impl ImagLogger {
|
||||||
impl Log for ImagLogger {
|
impl Log for ImagLogger {
|
||||||
|
|
||||||
fn enabled(&self, metadata: &LogMetadata) -> bool {
|
fn enabled(&self, metadata: &LogMetadata) -> bool {
|
||||||
metadata.level() <= self.lvl
|
metadata.level() <= self.global_loglevel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn log(&self, record: &LogRecord) {
|
fn log(&self, record: &LogRecord) {
|
||||||
use ansi_term::Colour::Red;
|
if record.location().module_path().starts_with("handlebars") {
|
||||||
use ansi_term::Colour::Yellow;
|
// This is a ugly, yet necessary hack. When logging, we use handlebars for templating.
|
||||||
use ansi_term::Colour::Cyan;
|
// But as the handlebars library itselfs logs via a normal logging macro ("debug!()"),
|
||||||
|
// we have a recursion in our chain.
|
||||||
if self.enabled(record.metadata()) {
|
//
|
||||||
// TODO: This is just simple logging. Maybe we can enhance this lateron
|
// To prevent this recursion, we return here.
|
||||||
let loc = record.location();
|
//
|
||||||
match record.metadata().level() {
|
// (As of handlebars 0.29.0 - please check whether you can update handlebars if you see
|
||||||
LogLevel::Debug => {
|
// this. Hopefully the next version has a compiletime flag to disable logging)
|
||||||
let lvl = self.color_or_not(Cyan, format!("{}", record.level()));
|
return;
|
||||||
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()));
|
|
||||||
|
|
||||||
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()));
|
|
||||||
|
|
||||||
writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok();
|
let mut data = BTreeMap::new();
|
||||||
},
|
|
||||||
LogLevel::Info => {
|
|
||||||
let lvl = self.color_or_not(Yellow, format!("{}", record.level()));
|
|
||||||
let args = self.color_or_not(Yellow, format!("{}", record.args()));
|
|
||||||
|
|
||||||
writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, lvl, args).ok();
|
{
|
||||||
|
data.insert("level", format!("{}", record.level()));
|
||||||
|
data.insert("module_path", String::from(record.location().module_path()));
|
||||||
|
data.insert("file", String::from(record.location().file()));
|
||||||
|
data.insert("line", format!("{}", record.location().line()));
|
||||||
|
data.insert("target", String::from(record.target()));
|
||||||
|
data.insert("message", format!("{}", record.args()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let logtext = self
|
||||||
|
.handlebars
|
||||||
|
.render(&format!("{}", record.level()), &data)
|
||||||
|
.unwrap_or_else(|e| format!("Failed rendering logging data: {:?}\n", e));
|
||||||
|
|
||||||
|
self.module_settings
|
||||||
|
.get(record.target())
|
||||||
|
.map(|module_setting| {
|
||||||
|
let set = module_setting.enabled &&
|
||||||
|
module_setting.level.unwrap_or(self.global_loglevel) >= record.level();
|
||||||
|
|
||||||
|
if set {
|
||||||
|
let _ = write!(stderr(), "{}\n", logtext);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
if self.global_loglevel >= record.level() {
|
||||||
|
// Yes, we log
|
||||||
|
let _ = write!(stderr(), "{}\n", logtext);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_log_level_str(s: &str) -> Result<LogLevel> {
|
||||||
|
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()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_global_loglevel(matches: &ArgMatches, config: Option<&Configuration>)
|
||||||
|
-> Result<LogLevel>
|
||||||
|
{
|
||||||
|
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 => {
|
||||||
writeln!(stderr(), "{}[{: <5}]: {}", self.prefix, record.level(), record.args()).ok();
|
if matches.is_present(Runtime::arg_debugging_name()) {
|
||||||
|
return Ok(LogLevel::Debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches
|
||||||
|
.value_of(Runtime::arg_verbosity_name())
|
||||||
|
.map(match_log_level_str)
|
||||||
|
.unwrap_or(Ok(LogLevel::Info))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn translate_destination(raw: &str) -> Result<LogDestination> {
|
||||||
|
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<Value>) -> Result<Vec<LogDestination>> {
|
||||||
|
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<Vec<LogDestination>>
|
||||||
|
{
|
||||||
|
|
||||||
|
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::GlobalDestinationConfigMissing.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() ])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn aggregate_global_format(
|
||||||
|
read_str: &str,
|
||||||
|
cli_match_name: &str,
|
||||||
|
error_kind_if_missing: EK,
|
||||||
|
matches: &ArgMatches,
|
||||||
|
config: Option<&Configuration>
|
||||||
|
)
|
||||||
|
-> Result<String>
|
||||||
|
{
|
||||||
|
match config {
|
||||||
|
Some(cfg) => match cfg
|
||||||
|
.read(read_str)
|
||||||
|
.map_err_into(EK::ConfigReadError)
|
||||||
|
{
|
||||||
|
Ok(Some(&Value::String(ref s))) => Ok(s.clone()),
|
||||||
|
Ok(Some(_)) => Err(EK::ConfigTypeError.into_error()),
|
||||||
|
Ok(None) => Err(error_kind_if_missing.into_error()),
|
||||||
|
Err(e) => Err(e)
|
||||||
|
},
|
||||||
|
None => match matches.value_of(cli_match_name).map(String::from) {
|
||||||
|
Some(s) => Ok(s),
|
||||||
|
None => Err(error_kind_if_missing.into_error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_global_format_trace(matches: &ArgMatches, config: Option<&Configuration>)
|
||||||
|
-> Result<String>
|
||||||
|
{
|
||||||
|
aggregate_global_format("imag.logging.format.trace",
|
||||||
|
Runtime::arg_override_trace_logging_format(),
|
||||||
|
EK::ConfigMissingLoggingFormatTrace,
|
||||||
|
matches,
|
||||||
|
config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_global_format_debug(matches: &ArgMatches, config: Option<&Configuration>)
|
||||||
|
-> Result<String>
|
||||||
|
{
|
||||||
|
aggregate_global_format("imag.logging.format.debug",
|
||||||
|
Runtime::arg_override_debug_logging_format(),
|
||||||
|
EK::ConfigMissingLoggingFormatDebug,
|
||||||
|
matches,
|
||||||
|
config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_global_format_info(matches: &ArgMatches, config: Option<&Configuration>)
|
||||||
|
-> Result<String>
|
||||||
|
{
|
||||||
|
aggregate_global_format("imag.logging.format.info",
|
||||||
|
Runtime::arg_override_info_logging_format(),
|
||||||
|
EK::ConfigMissingLoggingFormatInfo,
|
||||||
|
matches,
|
||||||
|
config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_global_format_warn(matches: &ArgMatches, config: Option<&Configuration>)
|
||||||
|
-> Result<String>
|
||||||
|
{
|
||||||
|
aggregate_global_format("imag.logging.format.warn",
|
||||||
|
Runtime::arg_override_warn_logging_format(),
|
||||||
|
EK::ConfigMissingLoggingFormatWarn,
|
||||||
|
matches,
|
||||||
|
config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_global_format_error(matches: &ArgMatches, config: Option<&Configuration>)
|
||||||
|
-> Result<String>
|
||||||
|
{
|
||||||
|
aggregate_global_format("imag.logging.format.error",
|
||||||
|
Runtime::arg_override_error_logging_format(),
|
||||||
|
EK::ConfigMissingLoggingFormatError,
|
||||||
|
matches,
|
||||||
|
config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn aggregate_module_settings(_matches: &ArgMatches, config: Option<&Configuration>)
|
||||||
|
-> Result<BTreeMap<ModuleName, ModuleSettings>>
|
||||||
|
{
|
||||||
|
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).map(Some),
|
||||||
|
Ok(None) => Ok(None),
|
||||||
|
Ok(Some(_)) => Err(EK::ConfigTypeError.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).map(Some),
|
||||||
|
Ok(None) => Ok(None),
|
||||||
|
Ok(Some(_)) => Err(EK::ConfigTypeError.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(None) => Ok(false),
|
||||||
|
Ok(Some(_)) => Err(EK::ConfigTypeError.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) => {
|
||||||
|
// No modules configured. This is okay!
|
||||||
|
Ok(BTreeMap::new())
|
||||||
|
},
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mod template_helpers {
|
||||||
|
use handlebars::{Handlebars, HelperDef, JsonRender, RenderError, RenderContext, Helper};
|
||||||
|
use ansi_term::Colour;
|
||||||
|
use ansi_term::Style;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizeBlackHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizeBlackHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::Black, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizeBlueHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizeBlueHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::Blue, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizeCyanHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizeCyanHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::Cyan, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizeGreenHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizeGreenHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::Green, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizePurpleHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizePurpleHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::Purple, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizeRedHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizeRedHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::Red, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizeWhiteHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizeWhiteHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::White, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct ColorizeYellowHelper;
|
||||||
|
|
||||||
|
impl HelperDef for ColorizeYellowHelper {
|
||||||
|
fn call(&self, h: &Helper, hb: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
colorize(Colour::Yellow, h, hb, rc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn colorize(color: Colour, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(), RenderError> {
|
||||||
|
let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments")));
|
||||||
|
|
||||||
|
try!(write!(rc.writer(), "{}", color.paint(p.value().render())));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct UnderlineHelper;
|
||||||
|
|
||||||
|
impl HelperDef for UnderlineHelper {
|
||||||
|
fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(),
|
||||||
|
RenderError> {
|
||||||
|
let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments")));
|
||||||
|
let s = Style::new().underline();
|
||||||
|
try!(write!(rc.writer(), "{}", s.paint(p.value().render())));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct BoldHelper;
|
||||||
|
|
||||||
|
impl HelperDef for BoldHelper {
|
||||||
|
fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(),
|
||||||
|
RenderError> {
|
||||||
|
let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments")));
|
||||||
|
let s = Style::new().bold();
|
||||||
|
try!(write!(rc.writer(), "{}", s.paint(p.value().render())));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct BlinkHelper;
|
||||||
|
|
||||||
|
impl HelperDef for BlinkHelper {
|
||||||
|
fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(),
|
||||||
|
RenderError> {
|
||||||
|
let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments")));
|
||||||
|
let s = Style::new().blink();
|
||||||
|
try!(write!(rc.writer(), "{}", s.paint(p.value().render())));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct StrikethroughHelper;
|
||||||
|
|
||||||
|
impl HelperDef for StrikethroughHelper {
|
||||||
|
fn call(&self, h: &Helper, _: &Handlebars, rc: &mut RenderContext) -> Result<(),
|
||||||
|
RenderError> {
|
||||||
|
let p = try!(h.param(0).ok_or(RenderError::new("Too few arguments")));
|
||||||
|
let s = Style::new().strikethrough();
|
||||||
|
try!(write!(rc.writer(), "{}", s.paint(p.value().render())));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,12 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::io::stderr;
|
use std::process::exit;
|
||||||
use std::io::Write;
|
|
||||||
|
|
||||||
pub use clap::App;
|
pub use clap::App;
|
||||||
|
|
||||||
use clap::{Arg, ArgMatches};
|
use clap::{Arg, ArgMatches};
|
||||||
use log;
|
use log;
|
||||||
use log::LogLevelFilter;
|
|
||||||
|
|
||||||
use configuration::{Configuration, InternalConfiguration};
|
use configuration::{Configuration, InternalConfiguration};
|
||||||
use error::RuntimeError;
|
use error::RuntimeError;
|
||||||
|
@ -35,6 +33,7 @@ use error::RuntimeErrorKind;
|
||||||
use error::MapErrInto;
|
use error::MapErrInto;
|
||||||
use logger::ImagLogger;
|
use logger::ImagLogger;
|
||||||
|
|
||||||
|
use libimagerror::trace::*;
|
||||||
use libimagstore::store::Store;
|
use libimagstore::store::Store;
|
||||||
use libimagstore::file_abstraction::InMemoryFileAbstraction;
|
use libimagstore::file_abstraction::InMemoryFileAbstraction;
|
||||||
use spec::CliSpec;
|
use spec::CliSpec;
|
||||||
|
@ -78,8 +77,8 @@ impl<'a> Runtime<'a> {
|
||||||
Err(e) => if e.err_type() != ConfigErrorKind::NoConfigFileFound {
|
Err(e) => if e.err_type() != ConfigErrorKind::NoConfigFileFound {
|
||||||
return Err(RuntimeErrorKind::Instantiate.into_error_with_cause(Box::new(e)));
|
return Err(RuntimeErrorKind::Instantiate.into_error_with_cause(Box::new(e)));
|
||||||
} else {
|
} else {
|
||||||
warn!("No config file found.");
|
println!("No config file found.");
|
||||||
warn!("Continuing without configuration file");
|
println!("Continuing without configuration file");
|
||||||
None
|
None
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -112,16 +111,10 @@ impl<'a> Runtime<'a> {
|
||||||
where C: Clone + CliSpec<'a> + InternalConfiguration
|
where C: Clone + CliSpec<'a> + InternalConfiguration
|
||||||
{
|
{
|
||||||
use std::io::stdout;
|
use std::io::stdout;
|
||||||
|
|
||||||
use clap::Shell;
|
use clap::Shell;
|
||||||
|
|
||||||
let is_debugging = matches.is_present(Runtime::arg_debugging_name());
|
|
||||||
|
|
||||||
if cli_app.enable_logging() {
|
if cli_app.enable_logging() {
|
||||||
let is_verbose = matches.is_present(Runtime::arg_verbosity_name());
|
Runtime::init_logger(&matches, config.as_ref())
|
||||||
let colored = !matches.is_present(Runtime::arg_no_color_output_name());
|
|
||||||
|
|
||||||
Runtime::init_logger(is_debugging, is_verbose, colored);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
match matches.value_of(Runtime::arg_generate_compl()) {
|
match matches.value_of(Runtime::arg_generate_compl()) {
|
||||||
|
@ -151,9 +144,9 @@ impl<'a> Runtime<'a> {
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_debugging {
|
if matches.is_present(Runtime::arg_debugging_name()) {
|
||||||
write!(stderr(), "Config: {:?}\n", config).ok();
|
debug!("Config: {:?}\n", config);
|
||||||
write!(stderr(), "Store-config: {:?}\n", store_config).ok();
|
debug!("Store-config: {:?}\n", store_config);
|
||||||
}
|
}
|
||||||
|
|
||||||
let store_result = if cli_app.use_inmemory_fs() {
|
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())
|
.arg(Arg::with_name(Runtime::arg_verbosity_name())
|
||||||
.short("v")
|
.short("v")
|
||||||
.long("verbose")
|
.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)
|
.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())
|
.arg(Arg::with_name(Runtime::arg_debugging_name())
|
||||||
.long("debug")
|
.long("debug")
|
||||||
|
@ -254,6 +249,61 @@ impl<'a> Runtime<'a> {
|
||||||
.value_name("SHELL")
|
.value_name("SHELL")
|
||||||
.possible_values(&["bash", "fish", "zsh"]))
|
.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: <modulename>=<setting>=<value>, whereas <setting> is either 'enabled', 'level' or 'destinations' - This commandline argument is CURRENTLY NOT IMPLEMENTED")
|
||||||
|
.multiple(true)
|
||||||
|
.required(false)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("SPEC"))
|
||||||
|
|
||||||
|
.arg(Arg::with_name(Runtime::arg_override_trace_logging_format())
|
||||||
|
.long(Runtime::arg_override_trace_logging_format())
|
||||||
|
.help("Override the logging format for the trace logging")
|
||||||
|
.multiple(false)
|
||||||
|
.required(false)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("FMT"))
|
||||||
|
|
||||||
|
.arg(Arg::with_name(Runtime::arg_override_debug_logging_format())
|
||||||
|
.long(Runtime::arg_override_debug_logging_format())
|
||||||
|
.help("Override the logging format for the debug logging")
|
||||||
|
.multiple(false)
|
||||||
|
.required(false)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("FMT"))
|
||||||
|
|
||||||
|
.arg(Arg::with_name(Runtime::arg_override_info_logging_format())
|
||||||
|
.long(Runtime::arg_override_info_logging_format())
|
||||||
|
.help("Override the logging format for the info logging")
|
||||||
|
.multiple(false)
|
||||||
|
.required(false)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("FMT"))
|
||||||
|
|
||||||
|
.arg(Arg::with_name(Runtime::arg_override_warn_logging_format())
|
||||||
|
.long(Runtime::arg_override_warn_logging_format())
|
||||||
|
.help("Override the logging format for the warn logging")
|
||||||
|
.multiple(false)
|
||||||
|
.required(false)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("FMT"))
|
||||||
|
|
||||||
|
.arg(Arg::with_name(Runtime::arg_override_error_logging_format())
|
||||||
|
.long(Runtime::arg_override_error_logging_format())
|
||||||
|
.help("Override the logging format for the error logging")
|
||||||
|
.multiple(false)
|
||||||
|
.required(false)
|
||||||
|
.takes_value(true)
|
||||||
|
.value_name("FMT"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the argument names of the Runtime which are available
|
/// Get the argument names of the Runtime which are available
|
||||||
|
@ -338,26 +388,50 @@ impl<'a> Runtime<'a> {
|
||||||
self
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_override_trace_logging_format() -> &'static str {
|
||||||
|
"override-logging-format-trace"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_override_debug_logging_format() -> &'static str {
|
||||||
|
"override-logging-format-debug"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_override_info_logging_format() -> &'static str {
|
||||||
|
"override-logging-format-info"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_override_warn_logging_format() -> &'static str {
|
||||||
|
"override-logging-format-warn"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arg_override_error_logging_format() -> &'static str {
|
||||||
|
"override-logging-format-error"
|
||||||
|
}
|
||||||
|
|
||||||
/// Initialize the internal logger
|
/// 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 std::env::var as env_var;
|
||||||
use env_logger;
|
use env_logger;
|
||||||
|
|
||||||
if env_var("IMAG_LOG_ENV").is_ok() {
|
if env_var("IMAG_LOG_ENV").is_ok() {
|
||||||
env_logger::init().unwrap();
|
env_logger::init().unwrap();
|
||||||
} else {
|
} else {
|
||||||
let lvl = if is_debugging {
|
|
||||||
LogLevelFilter::Debug
|
|
||||||
} else if is_verbose {
|
|
||||||
LogLevelFilter::Info
|
|
||||||
} else {
|
|
||||||
LogLevelFilter::Warn
|
|
||||||
};
|
|
||||||
|
|
||||||
log::set_logger(|max_log_lvl| {
|
log::set_logger(|max_log_lvl| {
|
||||||
max_log_lvl.set(lvl);
|
let logger = ImagLogger::new(matches, config)
|
||||||
debug!("Init logger with {}", lvl);
|
.map_err_trace()
|
||||||
Box::new(ImagLogger::new(lvl.to_log_level().unwrap()).with_color(colored))
|
.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))
|
.map_err(|e| panic!("Could not setup logger: {:?}", e))
|
||||||
.ok();
|
.ok();
|
||||||
|
|
Loading…
Reference in a new issue