From a3bc85bb5fba0d29f5243e5451cd8088b84af0c8 Mon Sep 17 00:00:00 2001 From: Devyn Cairns Date: Wed, 5 Jun 2024 01:42:55 -0700 Subject: [PATCH] Add options for filtering the log output from `nu` (#13044) # Description Add `--log-include` and `--log-exclude` options to filter the log output from `nu` to specific module prefixes. For example, ```nushell nu --log-level trace --log-exclude '[nu_parser]' ``` This avoids having to scan through parser spam when trying to debug something else at `TRACE` level, and should make it feel much more reasonable to add logging, particularly at `TRACE` level, to various places in the codebase. It can also be used to debug non-Nushell crates that support the Rust logging infrastructure, as many do. You can also include a specific module instead of excluding the parser log output: ```nushell nu --log-level trace --log-include '[nu_plugin]' ``` Pinging #13041 for reference, but hesitant to outright say that this closes that. I think it address that concern though. I've also struggled with debugging plugin stuff with all of the other output, so this will really help me there. # User-Facing Changes - New `nu` option: `--log-include` - New `nu` option: `--log-exclude` --- src/command.rs | 76 +++++++++++++++++++++++++++++++++----------------- src/logger.rs | 21 +++++++++++++- src/main.rs | 25 +++++++++++++++-- 3 files changed, 93 insertions(+), 29 deletions(-) diff --git a/src/command.rs b/src/command.rs index 7fcf2da1b3..15da0e3e47 100644 --- a/src/command.rs +++ b/src/command.rs @@ -33,9 +33,9 @@ pub(crate) fn gather_commandline_args() -> (Vec, String, Vec) { | "--env-config" | "-I" | "ide-ast" => args.next().map(|a| escape_quote_string(&a)), #[cfg(feature = "plugin")] "--plugin-config" => args.next().map(|a| escape_quote_string(&a)), - "--log-level" | "--log-target" | "--testbin" | "--threads" | "-t" - | "--include-path" | "--lsp" | "--ide-goto-def" | "--ide-hover" | "--ide-complete" - | "--ide-check" => args.next(), + "--log-level" | "--log-target" | "--log-include" | "--log-exclude" | "--testbin" + | "--threads" | "-t" | "--include-path" | "--lsp" | "--ide-goto-def" + | "--ide-hover" | "--ide-complete" | "--ide-check" => args.next(), #[cfg(feature = "plugin")] "--plugins" => args.next(), _ => None, @@ -97,6 +97,8 @@ pub(crate) fn parse_commandline_args( let env_file = call.get_flag_expr("env-config"); let log_level = call.get_flag_expr("log-level"); let log_target = call.get_flag_expr("log-target"); + let log_include = call.get_flag_expr("log-include"); + let log_exclude = call.get_flag_expr("log-exclude"); let execute = call.get_flag_expr("execute"); let table_mode: Option = call.get_flag(engine_state, &mut stack, "table-mode")?; @@ -155,39 +157,47 @@ pub(crate) fn parse_commandline_args( } } + fn extract_list( + expression: Option<&Expression>, + type_name: &str, + mut extract_item: impl FnMut(&Expression) -> Option, + ) -> Result>>, ShellError> { + expression + .map(|expr| match &expr.expr { + Expr::List(list) => list + .iter() + .map(|item| { + extract_item(item.expr()) + .map(|s| s.into_spanned(item.expr().span)) + .ok_or_else(|| ShellError::TypeMismatch { + err_message: type_name.into(), + span: item.expr().span, + }) + }) + .collect::>, _>>(), + _ => Err(ShellError::TypeMismatch { + err_message: format!("list<{type_name}>"), + span: expr.span, + }), + }) + .transpose() + } + let commands = extract_contents(commands)?; let testbin = extract_contents(testbin)?; #[cfg(feature = "plugin")] let plugin_file = extract_path(plugin_file)?; + #[cfg(feature = "plugin")] + let plugins = extract_list(plugins, "path", |expr| expr.as_filepath().map(|t| t.0))?; let config_file = extract_path(config_file)?; let env_file = extract_path(env_file)?; let log_level = extract_contents(log_level)?; let log_target = extract_contents(log_target)?; + let log_include = extract_list(log_include, "string", |expr| expr.as_string())?; + let log_exclude = extract_list(log_exclude, "string", |expr| expr.as_string())?; let execute = extract_contents(execute)?; let include_path = extract_contents(include_path)?; - #[cfg(feature = "plugin")] - let plugins = plugins - .map(|expr| match &expr.expr { - Expr::List(list) => list - .iter() - .map(|item| { - item.expr() - .as_filepath() - .map(|(s, _)| s.into_spanned(item.expr().span)) - .ok_or_else(|| ShellError::TypeMismatch { - err_message: "path".into(), - span: item.expr().span, - }) - }) - .collect::>, _>>(), - _ => Err(ShellError::TypeMismatch { - err_message: "list".into(), - span: expr.span, - }), - }) - .transpose()?; - let help = call.has_flag(engine_state, &mut stack, "help")?; if help { @@ -224,6 +234,8 @@ pub(crate) fn parse_commandline_args( env_file, log_level, log_target, + log_include, + log_exclude, execute, include_path, ide_goto_def, @@ -262,6 +274,8 @@ pub(crate) struct NushellCliArgs { pub(crate) env_file: Option>, pub(crate) log_level: Option>, pub(crate) log_target: Option>, + pub(crate) log_include: Option>>, + pub(crate) log_exclude: Option>>, pub(crate) execute: Option>, pub(crate) table_mode: Option, pub(crate) no_newline: Option>, @@ -403,6 +417,18 @@ impl Command for Nu { "set the target for the log to output. stdout, stderr(default), mixed or file", None, ) + .named( + "log-include", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "set the Rust module prefixes to include in the log output. default: [nu]", + None, + ) + .named( + "log-exclude", + SyntaxShape::List(Box::new(SyntaxShape::String)), + "set the Rust module prefixes to exclude from the log output", + None, + ) .switch( "stdin", "redirect standard input to a command (with `-c`) or a script file", diff --git a/src/logger.rs b/src/logger.rs index 9a1c040503..dd66b17444 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -66,9 +66,15 @@ fn set_write_logger(level: LevelFilter, config: Config, path: &Path) -> Result<( } } +pub struct Filters { + pub include: Option>, + pub exclude: Option>, +} + pub fn configure( level: &str, target: &str, + filters: Filters, builder: &mut ConfigBuilder, ) -> (LevelFilter, LogTarget) { let level = match Level::from_str(level) { @@ -77,7 +83,20 @@ pub fn configure( }; // Add allowed module filter - builder.add_filter_allow_str("nu"); + if let Some(include) = filters.include { + for filter in include { + builder.add_filter_allow(filter); + } + } else { + builder.add_filter_allow_str("nu"); + } + + // Add ignored module filter + if let Some(exclude) = filters.exclude { + for filter in exclude { + builder.add_filter_ignore(filter); + } + } // Set level padding builder.set_level_padding(LevelPadding::Right); diff --git a/src/main.rs b/src/main.rs index bc48a9f7de..f9fdff3ccf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -25,7 +25,8 @@ use nu_cmd_base::util::get_init_cwd; use nu_lsp::LanguageServer; use nu_path::canonicalize_with; use nu_protocol::{ - engine::EngineState, report_error_new, ByteStream, PipelineData, ShellError, Span, Value, + engine::EngineState, report_error_new, ByteStream, PipelineData, ShellError, Span, Spanned, + Value, }; use nu_std::load_standard_library; use nu_utils::utils::perf; @@ -155,7 +156,10 @@ fn main() -> Result<()> { let (args_to_nushell, script_name, args_to_script) = gather_commandline_args(); let parsed_nu_cli_args = parse_commandline_args(&args_to_nushell.join(" "), &mut engine_state) - .unwrap_or_else(|_| std::process::exit(1)); + .unwrap_or_else(|err| { + report_error_new(&engine_state, &err); + std::process::exit(1) + }); // keep this condition in sync with the branches at the end engine_state.is_interactive = parsed_nu_cli_args.interactive_shell.is_some() @@ -168,6 +172,8 @@ fn main() -> Result<()> { engine_state.history_enabled = parsed_nu_cli_args.no_history.is_none(); let use_color = engine_state.get_config().use_ansi_coloring; + + // Set up logger if let Some(level) = parsed_nu_cli_args .log_level .as_ref() @@ -187,7 +193,20 @@ fn main() -> Result<()> { .map(|target| target.item.clone()) .unwrap_or_else(|| "stderr".to_string()); - logger(|builder| configure(&level, &target, builder))?; + let make_filters = |filters: &Option>>| { + filters.as_ref().map(|filters| { + filters + .iter() + .map(|filter| filter.item.clone()) + .collect::>() + }) + }; + let filters = logger::Filters { + include: make_filters(&parsed_nu_cli_args.log_include), + exclude: make_filters(&parsed_nu_cli_args.log_exclude), + }; + + logger(|builder| configure(&level, &target, filters, builder))?; // info!("start logging {}:{}:{}", file!(), line!(), column!()); perf( "start logging",