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`
This commit is contained in:
Devyn Cairns 2024-06-05 01:42:55 -07:00 committed by GitHub
parent a9c2349ada
commit a3bc85bb5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 93 additions and 29 deletions

View File

@ -33,9 +33,9 @@ pub(crate) fn gather_commandline_args() -> (Vec<String>, String, Vec<String>) {
| "--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<Value> =
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<String>,
) -> Result<Option<Vec<Spanned<String>>>, 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::<Result<Vec<Spanned<String>>, _>>(),
_ => 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::<Result<Vec<Spanned<String>>, _>>(),
_ => Err(ShellError::TypeMismatch {
err_message: "list<path>".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<Spanned<String>>,
pub(crate) log_level: Option<Spanned<String>>,
pub(crate) log_target: Option<Spanned<String>>,
pub(crate) log_include: Option<Vec<Spanned<String>>>,
pub(crate) log_exclude: Option<Vec<Spanned<String>>>,
pub(crate) execute: Option<Spanned<String>>,
pub(crate) table_mode: Option<Value>,
pub(crate) no_newline: Option<Spanned<String>>,
@ -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",

View File

@ -66,9 +66,15 @@ fn set_write_logger(level: LevelFilter, config: Config, path: &Path) -> Result<(
}
}
pub struct Filters {
pub include: Option<Vec<String>>,
pub exclude: Option<Vec<String>>,
}
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);

View File

@ -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<Vec<Spanned<String>>>| {
filters.as_ref().map(|filters| {
filters
.iter()
.map(|filter| filter.item.clone())
.collect::<Vec<String>>()
})
};
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",