diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d9559443..f549b94e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ bump. Currently experimental: project bundling, project dependencies # Unreleased +* fix: `icp canister logs` no longer has its output formats swapped. `--json` emitted the human-readable lines and the default emitted JSON; now `--json` emits machine-readable JSON (terminated by a newline) and the default emits the human-readable lines. `--follow` was already correct and is unchanged. Scripts that relied on the inverted behavior — parsing the default output as JSON, or omitting `--json` to get JSON — must now pass `--json`. + # v1.3.0 * feat: a canister environment variable's value can now be read from a file, by writing `var: { path: }` in place of `var: value`. The path resolves against the canister's directory — including in an environment override, matching `init_args` — and surrounding whitespace is trimmed off the file's contents. The file is read when the project is loaded, so a missing file fails before anything is deployed. `icp project bundle` writes the value into the bundled manifest inline, rejecting a file outside the project as it does for other manifest file references. diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 78c66ac4e..31cae22c8 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -1,4 +1,4 @@ -use std::io::stdout; +use std::io::{Write as _, stdout}; use anyhow::{Context as _, anyhow}; use candid::Principal; @@ -179,17 +179,9 @@ async fn fetch_and_display_logs( .context("Failed to fetch canister logs")?; if args.json { - println!( - "{}", - result - .canister_log_records - .iter() - .map(format_log) - .format("\n") - ); - } else { + let mut out = stdout(); serde_json::to_writer( - stdout(), + &mut out, &JsonListRecord { log_records: result .canister_log_records @@ -202,6 +194,19 @@ async fn fetch_and_display_logs( .collect(), }, )?; + // Unlike the other `--json` sites in this module, terminate the document with a + // newline: it is the entirety of stdout, so without it the shell prompt lands + // mid-line. Covered by `canister_logs_single_fetch`. + writeln!(out)?; + } else { + println!( + "{}", + result + .canister_log_records + .iter() + .map(format_log) + .format("\n") + ); } Ok(()) diff --git a/crates/icp-cli/tests/canister_logs_tests.rs b/crates/icp-cli/tests/canister_logs_tests.rs index 49484f91a..c62a2c06b 100644 --- a/crates/icp-cli/tests/canister_logs_tests.rs +++ b/crates/icp-cli/tests/canister_logs_tests.rs @@ -74,7 +74,8 @@ async fn canister_logs_single_fetch() { .assert() .success(); - // Fetch logs + // Fetch logs: the default emits the human-readable "[idx. timestamp]: content" lines, + // not JSON. (Regression: this orientation was once swapped with `--json`.) ctx.icp() .current_dir(&project_dir) .args([ @@ -86,8 +87,45 @@ async fn canister_logs_single_fetch() { ]) .assert() .success() - .stdout(contains("Test message 1")) - .stdout(contains("Test message 2")); + .stdout(contains("]: Test message 1")) + .stdout(contains("]: Test message 2")) + .stdout(contains("log_records").not()); + + // With --json: parseable JSON in the JsonListRecord shape, terminated by a newline. + let json_stdout = ctx + .icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--json", + "--environment", + "random-environment", + ]) + .assert() + .success() + .get_output() + .stdout + .clone(); + let json_stdout = String::from_utf8(json_stdout).expect("stdout is not valid UTF-8"); + assert!( + json_stdout.ends_with('\n'), + "--json output should end with a newline, got {json_stdout:?}" + ); + let parsed: serde_json::Value = + serde_json::from_str(json_stdout.trim()).expect("--json output should be valid JSON"); + let records = parsed["log_records"] + .as_array() + .expect("--json output should contain a log_records array"); + for message in ["Test message 1", "Test message 2"] { + let record = records + .iter() + .find(|r| r["content"].as_str() == Some(message)) + .unwrap_or_else(|| panic!("--json output should contain {message:?}")); + assert!(record["timestamp"].is_u64()); + assert!(record["index"].is_u64()); + } } #[cfg(unix)] // moc